feat(pdf): migrate PDF generation to @mintel/pdf package
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m3s
Build & Deploy / 🏗️ Build (push) Failing after 3m5s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-12 21:46:46 +01:00
parent 7225efb0ea
commit 0fed92ca8c
23 changed files with 101 additions and 2447 deletions

View File

@@ -4,6 +4,7 @@ import withMintelConfig from "@mintel/next-config";
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: 'standalone', output: 'standalone',
transpilePackages: ["@mintel/pdf"],
async rewrites() { async rewrites() {
const umamiUrl = const umamiUrl =
process.env.UMAMI_API_ENDPOINT || process.env.UMAMI_API_ENDPOINT ||

View File

@@ -31,6 +31,7 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
"@react-pdf/renderer": "^4.3.2", "@react-pdf/renderer": "^4.3.2",
"@remotion/bundler": "^4.0.414", "@remotion/bundler": "^4.0.414",
"@remotion/cli": "^4.0.414", "@remotion/cli": "^4.0.414",

View File

@@ -7,8 +7,8 @@ import { execSync } from "node:child_process";
import axios from "axios"; import axios from "axios";
import { FileCacheAdapter } from "../src/utils/cache/file-adapter.js"; import { FileCacheAdapter } from "../src/utils/cache/file-adapter.js";
import { initialState, PRICING } from "../src/logic/pricing/constants.js"; import { initialState, PRICING } from "@mintel/pdf";
import { calculateTotals } from "../src/logic/pricing/calculator.js"; import { calculateTotals } from "@mintel/pdf";
async function main() { async function main() {
const OPENROUTER_KEY = process.env.OPENROUTER_KEY; const OPENROUTER_KEY = process.env.OPENROUTER_KEY;

View File

@@ -4,9 +4,8 @@ import * as readline from "node:readline/promises";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { createElement } from "react"; import { createElement } from "react";
import { renderToFile } from "@react-pdf/renderer"; import { renderToFile } from "@react-pdf/renderer";
import { calculateTotals } from "../src/logic/pricing/calculator.js"; import { calculateTotals, initialState, PRICING } from "@mintel/pdf";
import { CombinedQuotePDF } from "../src/components/CombinedQuotePDF.js"; import { CombinedQuotePDF } from "../src/components/CombinedQuotePDF.js";
import { initialState, PRICING } from "../src/logic/pricing/constants.js";
import { import {
getTechDetails, getTechDetails,
getPrinciples, getPrinciples,

View File

@@ -13,8 +13,8 @@ import {
Footer, Footer,
FoldingMarks, FoldingMarks,
DocumentTitle, DocumentTitle,
} from "./pdf/SharedUI"; SimpleLayout,
import { SimpleLayout } from "./pdf/SimpleLayout"; } from "@mintel/pdf";
const localStyles = PDFStyleSheet.create({ const localStyles = PDFStyleSheet.create({
sectionContainer: { sectionContainer: {

View File

@@ -2,10 +2,12 @@
import * as React from "react"; import * as React from "react";
import { Document as PDFDocument } from "@react-pdf/renderer"; import { Document as PDFDocument } from "@react-pdf/renderer";
import { EstimationPDF } from "./EstimationPDF"; import {
import { AgbsPDF } from "./AgbsPDF"; EstimationPDF,
import { ClosingModule } from "./pdf/modules/CommonModules"; AgbsPDF,
import { SimpleLayout } from "./pdf/SimpleLayout"; ClosingModule,
SimpleLayout,
} from "@mintel/pdf";
interface CombinedProps { interface CombinedProps {
estimationProps: any; estimationProps: any;

View File

@@ -17,7 +17,7 @@ import * as confetti from "canvas-confetti";
import { FormState, Step } from "./ContactForm/types"; import { FormState, Step } from "./ContactForm/types";
import { PRICING, initialState } from "./ContactForm/constants"; import { PRICING, initialState } from "./ContactForm/constants";
import { calculateTotals } from "../logic/pricing/calculator"; import { calculateTotals } from "@mintel/pdf";
import { PriceCalculation } from "./ContactForm/components/PriceCalculation"; import { PriceCalculation } from "./ContactForm/components/PriceCalculation";
import { ShareModal } from "./ShareModal"; import { ShareModal } from "./ShareModal";

View File

@@ -49,7 +49,7 @@ export function PriceCalculation({
setPdfLoading(true); setPdfLoading(true);
try { try {
const { EstimationPDF } = await import("../../EstimationPDF"); const { EstimationPDF } = await import("@mintel/pdf");
const doc = ( const doc = (
<EstimationPDF <EstimationPDF
state={state} state={state}

View File

@@ -1,6 +1,7 @@
import { calculatePositions as logicCalculatePositions } from '../../logic/pricing'; import { calculatePositions as logicCalculatePositions } from "@mintel/pdf";
import { FormState } from './types'; import { FormState } from "./types";
export type { Position } from '../../logic/pricing'; export type { Position } from "@mintel/pdf";
export const calculatePositions = (state: FormState, pricing: any) => logicCalculatePositions(state as any, pricing); export const calculatePositions = (state: FormState, pricing: any) =>
logicCalculatePositions(state as any, pricing);

View File

@@ -1,157 +0,0 @@
"use client";
import * as React from "react";
import { calculatePositions } from "../logic/pricing";
import { Page as PDFPage } from "@react-pdf/renderer";
import { pdfStyles } from "./pdf/SharedUI";
import { DINLayout } from "./pdf/DINLayout";
import { SimpleLayout } from "./pdf/SimpleLayout";
// Modules
import { FrontPageModule } from "./pdf/modules/FrontPageModule";
import { BriefingModule } from "./pdf/modules/BriefingModule";
import { SitemapModule } from "./pdf/modules/SitemapModule";
import { EstimationModule } from "./pdf/modules/EstimationModule";
import {
TransparenzModule,
techPageModule as TechPageModule,
MaintenanceModule,
StandardsModule,
} from "./pdf/modules/CommonModules";
import { AboutModule, CrossSellModule } from "./pdf/modules/BrandingModules";
interface PDFProps {
state: any;
totalPrice: number;
monthlyPrice: number;
totalPagesCount: number;
pricing: any;
mode?: "estimation" | "full";
headerIcon?: string;
footerLogo?: string;
techDetails?: { t: string; d: string }[];
principles?: { t: string; d: string }[];
maintenanceDetails?: { t: string; d: string }[];
standardsDetails?: { t: string; d: string }[];
}
export const EstimationPDF = ({
state,
totalPrice,
pricing,
mode = "full",
headerIcon,
footerLogo,
techDetails,
principles,
maintenanceDetails,
standardsDetails,
...props
}: PDFProps) => {
const date = new Date().toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
});
const positions = calculatePositions(state, pricing);
const companyData = {
name: "Marc Mintel",
address1: "Georg-Meistermann-Straße 7",
address2: "54586 Schüller",
ustId: "DE367588065",
};
const bankData = {
name: "N26",
bic: "NTSBDEB1XXX",
iban: "DE50 1001 1001 2620 4328 65",
};
const commonProps = {
state,
date,
icon: headerIcon,
footerLogo,
companyData,
bankData,
};
if (mode === "estimation") {
return (
<DINLayout {...commonProps} showAddress={true} showFooterDetails={true}>
<EstimationModule
state={state}
positions={positions}
totalPrice={totalPrice}
date={date}
/>
</DINLayout>
);
}
// Full Portfolio Mode
let pageCounter = 1;
const getPageNum = () => (pageCounter++).toString().padStart(2, "0");
return (
<>
<PDFPage size="A4" style={pdfStyles.titlePage}>
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
</PDFPage>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<BriefingModule state={state} />
</SimpleLayout>
{state.sitemap && state.sitemap.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SitemapModule state={state} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<EstimationModule
state={state}
positions={positions}
totalPrice={totalPrice}
date={date}
/>
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TransparenzModule pricing={pricing} />
</SimpleLayout>
{standardsDetails && standardsDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<StandardsModule
standardsDetails={standardsDetails}
principles={principles}
/>
</SimpleLayout>
)}
{techDetails && techDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TechPageModule techDetails={techDetails} headerIcon={headerIcon} />
</SimpleLayout>
)}
{maintenanceDetails && maintenanceDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<MaintenanceModule maintenanceDetails={maintenanceDetails} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<AboutModule />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<CrossSellModule state={state} />
</SimpleLayout>
</>
);
};

View File

@@ -1,55 +0,0 @@
'use client';
import * as React from 'react';
import { Page as PDFPage } from '@react-pdf/renderer';
import { FoldingMarks, Header, Footer, pdfStyles } from './SharedUI';
interface DINLayoutProps {
children: React.ReactNode;
sender?: string;
recipient?: {
title: string;
subtitle?: string;
address?: string;
phone?: string;
email?: string;
taxId?: string;
};
icon?: string;
footerLogo?: string;
companyData: any;
bankData: any;
showAddress?: boolean;
showFooterDetails?: boolean;
}
export const DINLayout = ({
children,
sender,
recipient,
icon,
footerLogo,
companyData,
bankData,
showAddress = true,
showFooterDetails = true
}: DINLayoutProps) => {
return (
<PDFPage size="A4" style={pdfStyles.page}>
<FoldingMarks />
<Header
sender={sender}
recipient={recipient}
icon={icon}
showAddress={showAddress}
/>
{children}
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={showFooterDetails}
/>
</PDFPage>
);
};

View File

@@ -1,810 +0,0 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
Link as PDFLink,
Image as PDFImage,
Font,
} from "@react-pdf/renderer";
// 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 = {
HERO: 24, // Main Page Titles
HEADING: 14, // Section Headers
BODY: 11, // Standard Content
LABEL: 10, // Bold Labels / Keys
SMALL: 9, // Descriptions / Footnotes
TINY: 8, // Metadata / Unit prices
};
// Mintel Industrial Glyphs (strictly 1px stroke, 12x12px grid)
export const IndustrialGlyph = ({
type,
color = COLORS.TEXT_LIGHT,
size = 12,
}: {
type: string;
color?: string;
size?: number;
}) => {
const stroke = 1;
const scale = size / 12;
switch (type) {
case "base": // Skeletal cube base
return (
<PDFView style={{ width: size, height: size, position: "relative" }}>
<PDFView
style={{
position: "absolute",
top: 2 * scale,
left: 2 * scale,
width: 8 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
position: "absolute",
top: 0,
left: 0,
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
backgroundColor: "white",
}}
/>
</PDFView>
);
case "pages": // Layered rectangles
return (
<PDFView style={{ width: size, height: size, position: "relative" }}>
<PDFView
style={{
position: "absolute",
top: 3 * scale,
left: 3 * scale,
width: 6 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
position: "absolute",
top: 0,
left: 0,
width: 6 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
backgroundColor: "white",
}}
/>
</PDFView>
);
case "modules": // Four small squares grid
return (
<PDFView
style={{
width: size,
height: size,
flexDirection: "row",
flexWrap: "wrap",
gap: 2 * scale,
}}
>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
</PDFView>
);
case "logic": // Diamond with center point
return (
<PDFView
style={{
width: size,
height: size,
alignItems: "center",
justifyContent: "center",
}}
>
<PDFView
style={{
width: 8 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
transform: "rotate(45deg)",
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 2 * scale,
backgroundColor: color,
position: "absolute",
}}
/>
</PDFView>
);
case "interface": // Three horizontal lines of varying length
return (
<PDFView
style={{
width: size,
height: size,
justifyContent: "center",
gap: 2 * scale,
}}
>
<PDFView
style={{
width: 10 * scale,
height: stroke,
backgroundColor: color,
}}
/>
<PDFView
style={{ width: 6 * scale, height: stroke, backgroundColor: color }}
/>
<PDFView
style={{
width: 10 * scale,
height: stroke,
backgroundColor: color,
}}
/>
</PDFView>
);
case "management": // Framed grid
return (
<PDFView
style={{
width: size,
height: size,
borderWidth: stroke,
borderColor: color,
padding: 1 * scale,
}}
>
<PDFView
style={{
width: "100%",
height: 2 * scale,
backgroundColor: color,
marginBottom: 1 * scale,
}}
/>
<PDFView
style={{
width: "100%",
height: 2 * scale,
backgroundColor: color,
marginBottom: 1 * scale,
}}
/>
<PDFView
style={{ width: "100%", height: 2 * scale, backgroundColor: color }}
/>
</PDFView>
);
case "reveal": // Ascending bars
return (
<PDFView
style={{
width: size,
height: size,
flexDirection: "row",
alignItems: "flex-end",
gap: 1 * scale,
}}
>
<PDFView
style={{
width: 2 * scale,
height: 4 * scale,
backgroundColor: color,
opacity: 0.4,
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 7 * scale,
backgroundColor: color,
opacity: 0.7,
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 10 * scale,
backgroundColor: color,
}}
/>
</PDFView>
);
case "maintenance": // Circle with vertical notch
return (
<PDFView
style={{
width: size,
height: size,
borderRadius: 6 * scale,
borderWidth: stroke,
borderColor: color,
alignItems: "center",
}}
>
<PDFView
style={{
width: stroke,
height: 4 * scale,
backgroundColor: color,
marginTop: 1 * scale,
}}
/>
</PDFView>
);
default:
return (
<PDFView
style={{
width: size,
height: size,
borderWidth: stroke,
borderColor: COLORS.BLUEPRINT,
borderStyle: "dashed",
}}
/>
);
}
};
// 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: COLORS.WHITE,
fontFamily: "Helvetica",
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",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 20,
minHeight: 120,
},
addressBlock: {
width: "55%",
marginTop: 45, // DIN 5008 positioning for window
},
senderLine: {
fontSize: FONT_SIZES.TINY,
textDecoration: "underline",
color: COLORS.TEXT_DIM,
marginBottom: 8,
},
recipientAddress: {
fontSize: FONT_SIZES.BODY,
lineHeight: 1.4,
},
brandLogoContainer: {
width: "40%",
alignItems: "flex-end",
},
brandIconContainer: {
width: 40,
height: 40,
backgroundColor: "#0f172a",
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
marginBottom: 12,
},
brandIconText: {
color: COLORS.WHITE,
fontSize: 20,
fontWeight: "bold",
},
titleInfo: {
marginBottom: 24,
},
mainTitle: {
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
marginBottom: 4,
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
},
subTitle: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
marginTop: 2,
lineHeight: 1.4,
},
section: {
marginBottom: 32,
},
sectionTitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: 1,
color: COLORS.TEXT_LIGHT,
marginBottom: 8,
},
footer: {
position: "absolute",
bottom: 32,
left: 70,
right: 57,
borderTopWidth: 1,
borderTopColor: COLORS.GRID,
paddingTop: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
},
footerColumn: {
flex: 1,
alignItems: "flex-start",
},
footerLogo: {
height: 20,
width: "auto",
objectFit: "contain",
marginBottom: 8,
},
footerText: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
lineHeight: 1.4,
},
// NEW LAYOUT PRIMITIVES
asymmetryContainer: {
flexDirection: "row",
gap: 32,
},
asymmetryLeft: {
width: "32%",
},
asymmetryRight: {
width: "63%",
},
specRow: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
},
specLabel: {
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
color: COLORS.TEXT_LIGHT,
textTransform: "uppercase",
letterSpacing: 0.5,
},
specValue: {
fontSize: FONT_SIZES.SMALL,
color: COLORS.CHARCOAL,
fontWeight: "bold",
},
blueprintBox: {
borderWidth: 1,
borderColor: COLORS.GRID,
padding: 16,
backgroundColor: "#fafafa",
},
footerLabel: {
fontWeight: "bold",
color: COLORS.TEXT_DIM,
},
pageNumber: {
fontSize: FONT_SIZES.TINY,
color: COLORS.DIVIDER,
fontWeight: "bold",
marginTop: 8,
textAlign: "right",
},
foldingMark: {
position: "absolute",
left: 20,
width: 10,
borderTopWidth: 0.5,
borderTopColor: COLORS.DIVIDER,
},
divider: {
width: "100%",
height: 1,
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.TINY,
color: COLORS.BLUEPRINT,
fontFamily: "Helvetica",
letterSpacing: 1,
},
// Atoms
industrialListItem: {
flexDirection: "row",
alignItems: "flex-start",
marginBottom: 6,
},
industrialBulletBox: {
width: 6,
height: 6,
backgroundColor: COLORS.DIVIDER,
marginRight: 8,
marginTop: 5,
},
industrialTitle: {
fontSize: FONT_SIZES.HERO,
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 6,
letterSpacing: 0, // Reset for clarity
},
industrialSubtitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: COLORS.TEXT_LIGHT,
textTransform: "uppercase",
marginBottom: 16,
letterSpacing: 2,
},
industrialTextLead: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.4,
marginBottom: 12,
},
industrialText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 8,
},
industrialCard: {
padding: 16,
borderWidth: 1,
borderColor: COLORS.BLUEPRINT,
marginBottom: 12,
},
industrialCardTitle: {
fontSize: FONT_SIZES.BODY + 1, // 10
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: 0.5,
},
darkBox: {
marginTop: 32,
padding: 24,
backgroundColor: COLORS.CHARCOAL,
color: COLORS.WHITE,
},
darkTitle: {
fontSize: FONT_SIZES.HERO,
fontWeight: "bold",
color: COLORS.WHITE,
marginBottom: 8,
},
darkText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT,
lineHeight: 1.4,
},
});
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} />
{children}
</PDFView>
);
export const IndustrialCard = ({
title,
children,
style = {},
}: {
title: string;
children: React.ReactNode;
style?: any;
}) => (
<PDFView style={[pdfStyles.industrialCard, style]}>
<PDFText style={pdfStyles.industrialCardTitle}>{title}</PDFText>
{children}
</PDFView>
);
export const FoldingMarks = () => (
<>
<PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed />
</>
);
export const Divider = ({ style = {} }: { style?: any }) => (
<PDFView style={[pdfStyles.divider, style]} />
);
export const Footer = ({
logo,
companyData,
bankData,
showDetails = true,
showPageNumber = true,
}: {
logo?: string;
companyData: any;
bankData: any;
showDetails?: boolean;
showPageNumber?: boolean;
}) => (
<PDFView style={pdfStyles.footer}>
<PDFView style={pdfStyles.footerColumn}>
{logo ? (
<PDFImage src={logo} style={pdfStyles.footerLogo} />
) : (
<PDFText style={{ fontSize: 12, fontWeight: "bold", marginBottom: 8 }}>
marc mintel
</PDFText>
)}
</PDFView>
{showDetails && (
<>
<PDFView style={pdfStyles.footerColumn}>
<PDFText style={pdfStyles.footerText}>
<PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>
{"\n"}
{companyData.address1}
{"\n"}
{companyData.address2}
{"\n"}UST: {companyData.ustId}
</PDFText>
</PDFView>
<PDFView style={[pdfStyles.footerColumn, { alignItems: "flex-end" }]}>
{showPageNumber && (
<PDFText
style={pdfStyles.pageNumber}
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
)}
</PDFView>
</>
)}
{!showDetails && (
<PDFView style={[pdfStyles.footerColumn, { alignItems: "flex-end" }]}>
{showPageNumber && (
<PDFText
style={pdfStyles.pageNumber}
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
)}
</PDFView>
)}
</PDFView>
);
export const Header = ({
sender,
recipient,
icon,
showAddress = true,
}: {
sender?: string;
recipient?: {
title: string;
subtitle?: string;
email?: string;
address?: string;
phone?: string;
taxId?: string;
};
icon?: string;
showAddress?: boolean;
}) => (
<PDFView
style={[
pdfStyles.header,
showAddress ? {} : { minHeight: 40, marginBottom: 0 },
]}
>
<PDFView style={pdfStyles.addressBlock}>
{showAddress && sender && (
<>
<PDFText style={pdfStyles.senderLine}>{sender}</PDFText>
{recipient && (
<PDFView style={pdfStyles.recipientAddress}>
<PDFText style={{ fontWeight: "bold" }}>
{recipient.title}
</PDFText>
{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}
{recipient.address && <PDFText>{recipient.address}</PDFText>}
{recipient.phone && <PDFText>{recipient.phone}</PDFText>}
{recipient.email && <PDFText>{recipient.email}</PDFText>}
{recipient.taxId && <PDFText>USt-ID: {recipient.taxId}</PDFText>}
</PDFView>
)}
</>
)}
</PDFView>
<PDFView style={pdfStyles.brandLogoContainer}>
<PDFView style={pdfStyles.brandIconContainer}>
{icon ? (
<PDFImage src={icon} style={{ width: 24, height: 24 }} />
) : (
<PDFText style={pdfStyles.brandIconText}>M</PDFText>
)}
</PDFView>
</PDFView>
</PDFView>
);
export const DocumentTitle = ({
title,
subLines,
isHero = false,
}: {
title: string;
subLines?: string[];
isHero?: boolean;
}) => (
<PDFView style={pdfStyles.titleInfo}>
<PDFText
style={[
pdfStyles.mainTitle,
{ fontSize: isHero ? FONT_SIZES.HERO : FONT_SIZES.HEADING },
]}
>
{title}
</PDFText>
{subLines?.map((line, i) => (
<PDFText
key={i}
style={[
pdfStyles.subTitle,
i === 1 ? { fontWeight: "bold", color: COLORS.CHARCOAL } : {},
]}
>
{line}
</PDFText>
))}
</PDFView>
);
export const TechnicalSpec = ({
label,
value,
}: {
label: string;
value: string;
}) => (
<PDFView style={pdfStyles.specRow}>
<PDFText style={pdfStyles.specLabel}>{label}</PDFText>
<PDFText style={pdfStyles.specValue}>{value}</PDFText>
</PDFView>
);
export const AsymmetryView = ({
left,
right,
style,
}: {
left: React.ReactNode;
right: React.ReactNode;
style?: any;
}) => (
<PDFView style={[pdfStyles.asymmetryContainer, style]}>
<PDFView style={pdfStyles.asymmetryLeft}>{left}</PDFView>
<PDFView style={pdfStyles.asymmetryRight}>{right}</PDFView>
</PDFView>
);

View File

@@ -1,68 +0,0 @@
'use client';
import * as React from 'react';
import { Page as PDFPage, View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { Header, Footer, pdfStyles, BlueprintBackground } from './SharedUI';
const simpleStyles = StyleSheet.create({
industrialPage: {
padding: 30,
paddingTop: 20,
backgroundColor: '#ffffff',
},
industrialNumber: {
fontSize: 60,
fontWeight: 'bold',
color: '#f1f5f9',
position: 'absolute',
top: -10,
right: 0,
zIndex: -1,
},
industrialSection: {
marginTop: 16,
paddingTop: 12,
flexDirection: 'row',
position: 'relative',
},
});
interface SimpleLayoutProps {
children: React.ReactNode;
pageNumber?: string;
icon?: string;
footerLogo?: string;
companyData: any;
bankData: any;
showPageNumber?: boolean;
}
export const SimpleLayout = ({
children,
pageNumber,
icon,
footerLogo,
companyData,
bankData,
showPageNumber = true
}: 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}>
<PDFView style={{ width: '100%' }}>
{children}
</PDFView>
</PDFView>
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
showPageNumber={showPageNumber}
/>
</PDFPage>
);
};

View File

@@ -1,218 +0,0 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import {
DocumentTitle,
IndustrialListItem,
IndustrialCard,
Divider,
COLORS,
FONT_SIZES,
} from "../SharedUI";
const styles = StyleSheet.create({
industrialTextLead: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.4,
marginBottom: 16,
},
industrialText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 12,
},
industrialGrid2: { flexDirection: "row" },
industrialCol: { width: "46%" },
});
export const AboutModule = () => (
<>
<DocumentTitle
title="Expertise & Profil"
subLines={["Entwicklung & Technischer Partner für den Mittelstand"]}
isHero={true}
/>
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={{ marginTop: 24 }}>
<PDFText style={styles.industrialTextLead}>
Begleitung mittelständischer Unternehmen und Agenturen bei der
Realisierung anspruchsvoller Web-Projekte. Als Senior Software Developer
mit over 15 Jahren Erfahrung wird das gesamte technische Spektrum
abgedeckt von der Architektur bis zum fertigen Produkt.
</PDFText>
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
]}
>
Erfahrung & Substanz
</PDFText>
<PDFText style={styles.industrialText}>
Der Werdegang umfasst alle Ebenen der Webentwicklung: von der
Teamleitung in Kreativagenturen bis zur Softwareentwicklung für
internationale Konzerne.
</PDFText>
<PDFText style={styles.industrialText}>
Die Kenntnis komplexer Enterprise-Systeme wird mit der Agilität
kombiniert, die im Mittelstand gefordert ist. Dieses Wissen
ermöglicht den Bau von Lösungen, die technologisch auf Augenhöhe mit
Konzern-Standards sind, jedoch ohne unnötigen bürokratischen
Overhead auskommen.
</PDFText>
</PDFView>
<PDFView style={styles.industrialCol}>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 8 },
]}
>
Fokus Einzelentwicklung
</PDFText>
<PDFText style={styles.industrialText}>
Die Umsetzung erfolgt bewusst als spezialisierter Einzelentwickler.
Dies garantiert maximale Geschwindigkeit, direkte Kommunikationswege
und volle technologische Verantwortung.
</PDFText>
<PDFText style={styles.industrialText}>
Als direkter technischer Sparringspartner bleibt die Codebasis von
der ersten bis zur letzten Zeile transparent und wartbar. Diese
Unmittelbarkeit stellt sicher, dass Ergebnisse sowohl technisch
sauber als auch wirtschaftlich sinnvoll realisiert werden.
</PDFText>
</PDFView>
</PDFView>
<PDFView
style={{
marginTop: 32,
paddingVertical: 16,
borderTopWidth: 1,
borderTopColor: COLORS.GRID,
}}
>
<PDFText
style={[
styles.industrialText,
{ fontWeight: "bold", color: COLORS.CHARCOAL, marginBottom: 4 },
]}
>
Infrastruktur & Souveränität
</PDFText>
<PDFText style={styles.industrialText}>
Es wird keine instabile Prototyp-Software geliefert, sondern
produktionsreife Systeme, die technisch skalierbar bleiben. Die
Codebasis folgt modernen Standards bei wachsenden Ansprüchen oder
dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos
übernommen und weitergeführt werden.
</PDFText>
</PDFView>
</PDFView>
</>
);
export const CrossSellModule = ({ state }: any) => {
const isWebsite = state.projectType === "website";
const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
const subtitle = isWebsite
? "Automatisierung und Prozessoptimierung"
: "Technische Infrastruktur ohne Kompromisse";
return (
<>
<DocumentTitle title={title} subLines={[subtitle]} isHero={true} />
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 16 }]}>
{isWebsite ? (
<>
<PDFView style={[styles.industrialCol, { marginRight: "8%" }]}>
<PDFText style={styles.industrialTextLead}>
Über die klassische Webpräsenz hinaus werden maßgeschneiderte
Lösungen zur Automatisierung von Routine-Prozessen angeboten.
Dies ermöglicht eine signifikante Effizienzsteigerung im
Tagesgeschäft.
</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: "bold" }]}>
Keine Abos. Keine komplexen neuen Systeme. Gezielte
Zeitersparnis.
</PDFText>
<PDFView
style={{
marginTop: 24,
padding: 16,
backgroundColor: "#f8fafc",
borderLeftWidth: 2,
borderLeftColor: COLORS.GRID,
}}
>
<PDFText
style={[
styles.industrialText,
{
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 4,
},
]}
>
Individuelle Analyse
</PDFText>
<PDFText style={styles.industrialText}>
Spezifische Prozesse werden auf technisches
Automatisierungspotenzial untersucht. Das Ergebnis liefert
Klarheit über die wirtschaftliche Sinnhaftigkeit einer
Umsetzung.
</PDFText>
</PDFView>
</PDFView>
<PDFView style={styles.industrialCol}>
<IndustrialCard title="DOKUMENT-AUTOMATION">
<PDFText style={styles.industrialText}>
Erstellung von PDF-Angeboten, Berichten oder Protokollen in
Sekunden statt Stunden.
</PDFText>
</IndustrialCard>
<IndustrialCard title="EXCEL-LOGIK">
<PDFText style={styles.industrialText}>
Intelligente Tabellen und automatisierte Auswertungen
bestehender Datensätze.
</PDFText>
</IndustrialCard>
<IndustrialCard title="KI-ASSISTENZ">
<PDFText style={styles.industrialText}>
Effiziente Verarbeitung von analogen Dokumenten oder
handschriftlichen Notizen mittels KI.
</PDFText>
</IndustrialCard>
</PDFView>
</>
) : (
<PDFView style={{ width: "100%" }}>
<PDFText style={styles.industrialTextLead}>
Bereitstellung einer stabilen technischen Basis ohne
Abhängigkeiten von Baukasten-Systemen oder Agenturen.
</PDFText>
<PDFText style={styles.industrialText}>
Entwicklung performanter Frontends und skalierbarer Backends. Die
Auslieferung erfolgt als kontrollierbarer und nachhaltiger
Quellcode.
</PDFText>
</PDFView>
)}
</PDFView>
</>
);
};

View File

@@ -1,69 +0,0 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
section: { marginBottom: 24 },
sectionTitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
marginBottom: 8,
color: COLORS.CHARCOAL,
},
visionText: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.4,
textAlign: "justify",
},
});
export const BriefingModule = ({ state }: any) => (
<>
<DocumentTitle title="Projektdetails" isHero={true} />
{state.briefingSummary && (
<PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Briefing Analyse</PDFText>
<PDFText
style={{
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.6,
textAlign: "justify",
}}
>
{state.briefingSummary}
</PDFText>
</PDFView>
)}
{state.designVision && (
<PDFView
style={[
styles.section,
{
padding: 12,
borderLeftWidth: 2,
borderLeftColor: COLORS.DIVIDER,
backgroundColor: COLORS.GRID,
},
]}
>
<PDFText
style={[
styles.sectionTitle,
{ color: COLORS.CHARCOAL, marginBottom: 4 },
]}
>
Strategische Vision
</PDFText>
<PDFText style={styles.visionText}>{state.designVision}</PDFText>
</PDFView>
)}
</>
);

View File

@@ -1,443 +0,0 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
Image as PDFImage,
} from "@react-pdf/renderer";
import {
DocumentTitle,
Divider,
COLORS,
FONT_SIZES,
TechnicalSpec,
AsymmetryView,
} from "../SharedUI";
const styles = StyleSheet.create({
section: { marginBottom: 24 },
moduleLabel: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
marginBottom: 4,
},
moduleDesc: {
fontSize: FONT_SIZES.SMALL,
color: COLORS.TEXT_DIM,
lineHeight: 1.5,
},
ledgerRow: {
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
flexDirection: "row",
alignItems: "flex-start",
},
ledgerPrice: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
},
ledgerUnit: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginLeft: 2,
},
});
export const techPageModule = ({ techDetails }: any) => {
// Focus on the first 3 items as "Featured Specs", the rest as high-density grid
const featured = techDetails?.slice(0, 3) || [];
const rest = techDetails?.slice(3) || [];
return (
<>
<DocumentTitle title="Technische Umsetzung" isHero={true} />
<PDFView style={styles.section}>
{/* FEATURED SPECS - Editorial focus */}
{featured.map((item: any, i: number) => (
<PDFView key={i} style={{ marginBottom: 24 }}>
<PDFText
style={[
styles.moduleLabel,
{ color: COLORS.BLUEPRINT, fontSize: FONT_SIZES.TINY },
]}
>
FOKUS_{i + 1}
</PDFText>
<PDFText
style={[
styles.moduleLabel,
{ fontSize: FONT_SIZES.HEADING, marginBottom: 8 },
]}
>
{item.t}
</PDFText>
<PDFText
style={[
styles.moduleDesc,
{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN },
]}
>
{item.d}
</PDFText>
</PDFView>
))}
<Divider style={{ marginVertical: 24, backgroundColor: COLORS.GRID }} />
{/* TECHNICAL GRID - High density */}
<PDFView style={{ flexDirection: "row", flexWrap: "wrap", gap: 12 }}>
{rest.map((item: any, i: number) => (
<PDFView
key={i}
style={{
width: "31%",
padding: 10,
backgroundColor: "#fdfdfd",
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
}}
>
<PDFText
style={[
styles.moduleLabel,
{
fontSize: FONT_SIZES.TINY,
marginBottom: 4,
color: COLORS.TEXT_LIGHT,
},
]}
>
{item.t.toUpperCase()}
</PDFText>
<PDFText
style={[styles.moduleDesc, { fontSize: FONT_SIZES.TINY }]}
>
{item.d}
</PDFText>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);
};
export const MaintenanceModule = ({ maintenanceDetails }: any) => (
<>
<DocumentTitle
title="Monitoring, Security & techn. Support"
isHero={true}
/>
<PDFView style={styles.section}>
<PDFView style={{ flexDirection: "row", flexWrap: "wrap", gap: 16 }}>
{maintenanceDetails?.map((item: any, i: number) => (
<PDFView
key={i}
style={{
width: "48%",
padding: 16,
backgroundColor: COLORS.GRID,
borderLeftWidth: 2,
borderLeftColor: COLORS.DIVIDER,
}}
>
<PDFText
style={[
styles.moduleLabel,
{
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginBottom: 8,
},
]}
>
{item.t.toUpperCase()}
</PDFText>
<PDFText
style={[
styles.moduleDesc,
{
fontSize: FONT_SIZES.SMALL,
color: COLORS.CHARCOAL,
lineHeight: 1.4,
},
]}
>
{item.d}
</PDFText>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);
export const StandardsModule = ({ standardsDetails, principles }: any) => {
const independence = standardsDetails?.find(
(s: any) => s.t === "Unabhängigkeit",
);
const others =
standardsDetails?.filter((s: any) => s.t !== "Unabhängigkeit") || [];
return (
<>
<DocumentTitle title="Meine Standards" isHero={true} />
<PDFView style={styles.section}>
{/* FOCUS: UNABHÄNGIGKEIT & PRINCIPLES */}
<AsymmetryView
left={
<PDFView>
<PDFText
style={[
styles.moduleLabel,
{
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginBottom: 12,
},
]}
>
MODERNE PRINZIPIEN
</PDFText>
{principles?.map((p: any, i: number) => (
<PDFView key={i} style={{ marginBottom: 12 }}>
<PDFText
style={[styles.moduleLabel, { fontSize: FONT_SIZES.TINY }]}
>
{p.t.toUpperCase()}
</PDFText>
<PDFText
style={[
styles.moduleDesc,
{ fontSize: FONT_SIZES.TINY, opacity: 0.8 },
]}
>
{p.d}
</PDFText>
</PDFView>
))}
</PDFView>
}
right={
<PDFView>
{/* HERO BOX: UNABHÄNGIGKEIT */}
{independence && (
<PDFView
style={{
backgroundColor: COLORS.CHARCOAL,
padding: 24,
marginBottom: 32,
borderLeftWidth: 4,
borderLeftColor: COLORS.TEXT_LIGHT,
}}
>
<PDFText
style={{
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
color: COLORS.WHITE,
letterSpacing: 1,
marginBottom: 12,
}}
>
{independence.t.toUpperCase()}
</PDFText>
<PDFText
style={{
fontSize: FONT_SIZES.BODY,
color: COLORS.WHITE,
lineHeight: 1.4,
opacity: 0.9,
}}
>
{independence.d}
</PDFText>
</PDFView>
)}
{/* VERTICAL STACK OF OTHER STANDARDS */}
<PDFView>
{others.map((item: any, i: number) => (
<PDFView
key={i}
style={{
marginBottom: 16,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
paddingBottom: 12,
}}
>
<PDFText style={styles.moduleLabel}>{item.t}</PDFText>
<PDFText style={styles.moduleDesc}>{item.d}</PDFText>
</PDFView>
))}
</PDFView>
</PDFView>
}
/>
</PDFView>
</>
);
};
export const TransparenzModule = ({ pricing }: any) => {
const sorglosPrice = (pricing.HOSTING_MONTHLY || 250) * 12;
return (
<>
<DocumentTitle title="Preis-Transparenz & Modell" isHero={true} />
<PDFView style={styles.section}>
<PDFView style={{ borderTopWidth: 1, borderTopColor: COLORS.CHARCOAL }}>
{[
{
l: "Fundament",
d: "Bereitstellung der techn. Infrastruktur & System-Umgebung.",
p: pricing.BASE_WEBSITE,
},
{
l: "Einzelseiten",
d: "Individuelle Gestaltung, Layout & responsive Struktur.",
p: pricing.PAGE,
unit: "/ Stk",
},
{
l: "Core Features",
d: "Geschlossene Datensysteme mit eigener Datenstruktur.",
p: pricing.FEATURE,
unit: "/ Stk",
},
{
l: "Logik & Funktionen",
d: "Interaktive Funktions-Bausteine & Prozess-Logik.",
p: pricing.FUNCTION,
unit: "/ Stk",
},
{
l: "Schnittstellen",
d: "Synchronisation mit externen Zielsystemen.",
p: pricing.API_INTEGRATION,
unit: "/ Stk",
},
{
l: "Inhalts-Verwaltung",
d: "Schnittstelle zur eigenständigen Daten-Pflege (optional).",
p: pricing.CMS_CONNECTION_PER_FEATURE,
unit: "/ Stk",
},
{
l: "Sprachversionen",
d: "Skalierung der System-Architektur auf Zweit-Sprachen.",
p: "+20%",
isLang: true,
},
{
l: "Initial-Pflege",
d: "Konvertierung & Aufbereitung von Bestandsdaten.",
p: pricing.NEW_DATASET,
unit: "/ Stk",
},
{
l: "Sorglos Betrieb",
d: "Hosting, Instandhaltung, Security & techn. Support.",
p: sorglosPrice,
unit: "/ Jahr",
},
].map((item: any, i: number) => (
<PDFView key={i} style={styles.ledgerRow}>
<PDFView style={{ width: "25%" }}>
<PDFText style={styles.moduleLabel}>
{item.l.toUpperCase()}
</PDFText>
</PDFView>
<PDFView style={{ width: "50%" }}>
<PDFText style={styles.moduleDesc}>{item.d}</PDFText>
</PDFView>
<PDFView style={{ width: "25%", alignItems: "flex-end" }}>
<PDFText style={styles.ledgerPrice}>
{typeof item.p === "number"
? `${item.p.toLocaleString("de-DE")}`
: item.p}
{item.unit && (
<PDFText style={styles.ledgerUnit}> {item.unit}</PDFText>
)}
</PDFText>
{item.sub && (
<PDFText
style={[
styles.moduleDesc,
{
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
marginTop: 2,
},
]}
>
{item.sub}
</PDFText>
)}
</PDFView>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);
};
export const ClosingModule = () => (
<>
<DocumentTitle title="Abschluss & Kontakt" isHero={true} />
<PDFView style={styles.section}>
<PDFText
style={[
styles.moduleLabel,
{ fontSize: FONT_SIZES.HEADING, marginBottom: 12 },
]}
>
Vielen Dank für Ihr Interesse!
</PDFText>
<PDFText style={styles.moduleDesc}>
Die aufgeführten Positionen stellen eine detaillierte Schätzung auf
Basis unseres aktuellen Stands dar. Sollten sich Anforderungen ändern
oder Sie Fragen zu einzelnen Details haben, lassen Sie uns die
Positionen gerne gemeinsam besprechen.
</PDFText>
<PDFView
style={{
marginTop: 24,
padding: 16,
backgroundColor: COLORS.GRID,
borderLeftWidth: 2,
borderLeftColor: COLORS.DIVIDER,
}}
>
<PDFText style={[styles.moduleLabel, { marginBottom: 8 }]}>
Haben Sie Fragen?
</PDFText>
<PDFText style={styles.moduleDesc}>
Ich erkläre Ihnen gerne noch einmal persönlich, was die technische
Umsetzung für Ihr Projekt bedeutet und wie wir die nächsten Schritte
gemeinsam gehen können.
</PDFText>
<PDFView style={{ marginTop: 16 }}>
<PDFText style={styles.moduleLabel}>Kontakt:</PDFText>
<PDFText
style={[
styles.moduleDesc,
{ color: COLORS.CHARCOAL, fontWeight: "bold" },
]}
>
Marc Mintel marc@mintel.me
</PDFText>
</PDFView>
</PDFView>
</PDFView>
</>
);

View File

@@ -1,159 +0,0 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
table: { marginTop: 12 },
tableHeader: {
flexDirection: "row",
paddingBottom: 8,
borderBottomWidth: 1,
borderBottomColor: COLORS.CHARCOAL,
marginBottom: 12,
},
tableRow: {
flexDirection: "row",
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
alignItems: "flex-start",
},
colPos: { width: "8%" },
colDesc: { width: "62%" },
colQty: { width: "10%", textAlign: "center" },
colPrice: { width: "20%", textAlign: "right" },
headerText: {
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: 1,
color: COLORS.TEXT_DIM,
},
posText: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_LIGHT },
itemTitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 4,
},
itemDesc: {
fontSize: FONT_SIZES.SMALL,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
},
priceText: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
},
summaryContainer: {
borderTopWidth: 1,
borderTopColor: COLORS.CHARCOAL,
paddingTop: 8,
},
summaryRow: {
flexDirection: "row",
justifyContent: "flex-end",
paddingVertical: 4,
alignItems: "baseline",
},
summaryLabel: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_DIM,
textTransform: "uppercase",
letterSpacing: 1,
fontWeight: "bold",
marginRight: 12,
},
summaryValue: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
width: 100,
textAlign: "right",
color: COLORS.CHARCOAL,
},
totalRow: {
flexDirection: "row",
justifyContent: "flex-end",
paddingTop: 12,
marginTop: 8,
borderTopWidth: 2,
borderTopColor: COLORS.CHARCOAL,
alignItems: "baseline",
},
});
export const EstimationModule = ({
state,
positions,
totalPrice,
date,
}: any) => (
<>
<DocumentTitle
title="Kostenschätzung"
subLines={[
`Datum: ${date}`,
`Projekt: ${state.projectType === "website" ? "Website" : "Web App"}`,
]}
isHero={true}
/>
<PDFView style={styles.table}>
<PDFView style={styles.tableHeader}>
<PDFText style={[styles.headerText, styles.colPos]}>Pos</PDFText>
<PDFText style={[styles.headerText, styles.colDesc]}>
Beschreibung
</PDFText>
<PDFText style={[styles.headerText, styles.colQty]}>Menge</PDFText>
<PDFText style={[styles.headerText, styles.colPrice]}>Betrag</PDFText>
</PDFView>
{positions.map((item: any, i: number) => (
<PDFView key={i} style={styles.tableRow} wrap={false}>
<PDFText style={[styles.posText, styles.colPos]}>
{item.pos.toString().padStart(2, "0")}
</PDFText>
<PDFView style={styles.colDesc}>
<PDFText style={styles.itemTitle}>{item.title}</PDFText>
<PDFText style={styles.itemDesc}>
{state.positionDescriptions?.[item.title] || item.desc}
</PDFText>
</PDFView>
<PDFText style={[styles.posText, styles.colQty]}>{item.qty}</PDFText>
<PDFText style={[styles.priceText, styles.colPrice]}>
{item.price > 0
? `${item.price.toLocaleString("de-DE")}`
: "n. A."}
</PDFText>
</PDFView>
))}
</PDFView>
<PDFView style={styles.summaryContainer} wrap={false}>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>Nettobetrag</PDFText>
<PDFText style={styles.summaryValue}>
{totalPrice.toLocaleString("de-DE")}
</PDFText>
</PDFView>
<PDFView style={styles.summaryRow}>
<PDFText style={styles.summaryLabel}>Umsatzsteuer (19%)</PDFText>
<PDFText style={styles.summaryValue}>
{(totalPrice * 0.19).toLocaleString("de-DE")}
</PDFText>
</PDFView>
<PDFView style={styles.totalRow}>
<PDFText style={styles.summaryLabel}>Gesamtbetrag (Brutto)</PDFText>
<PDFText
style={[styles.summaryValue, { fontSize: FONT_SIZES.HEADING }]}
>
{(totalPrice * 1.19).toLocaleString("de-DE")}
</PDFText>
</PDFView>
</PDFView>
</>
);

View File

@@ -1,92 +0,0 @@
"use client";
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",
backgroundColor: COLORS.WHITE,
},
titleBrandIcon: {
width: 80,
height: 80,
backgroundColor: COLORS.CHARCOAL,
borderRadius: 16,
alignItems: "center",
justifyContent: "center",
marginBottom: 40,
},
brandIconText: {
fontSize: 40,
color: COLORS.WHITE,
fontWeight: "bold",
},
titleProjectName: {
fontSize: FONT_SIZES.HERO,
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 16,
textAlign: "center",
maxWidth: "85%",
lineHeight: 1.2,
},
titleCustomerName: {
fontSize: FONT_SIZES.HEADING,
color: COLORS.TEXT_DIM,
marginBottom: 40,
textAlign: "center",
maxWidth: "80%",
},
titleDocumentType: {
fontSize: FONT_SIZES.BODY + 1, // ~10
color: COLORS.TEXT_LIGHT,
textTransform: "uppercase",
letterSpacing: 4,
marginBottom: 12,
},
titleDivider: {
width: 40,
height: 2,
backgroundColor: COLORS.CHARCOAL,
marginBottom: 40,
},
titleDate: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT,
marginTop: 40,
},
});
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={{ marginBottom: 40 }} />
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
</PDFView>
);
};

View File

@@ -1,125 +0,0 @@
"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
} from "@react-pdf/renderer";
import { DocumentTitle, COLORS, FONT_SIZES } from "../SharedUI";
const styles = StyleSheet.create({
section: { marginBottom: 32 },
intro: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.4,
marginBottom: 24,
textAlign: "justify",
},
sitemapTree: { marginTop: 8 },
rootNode: {
padding: 12,
backgroundColor: COLORS.GRID,
marginBottom: 20,
borderLeftWidth: 2,
borderLeftColor: COLORS.CHARCOAL,
},
rootTitle: {
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
},
categorySection: { marginBottom: 20 },
categoryHeader: {
flexDirection: "row",
alignItems: "center",
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.BLUEPRINT,
marginBottom: 10,
},
categoryIcon: {
width: 8,
height: 8,
backgroundColor: COLORS.GRID,
borderInlineWidth: 1,
borderColor: COLORS.DIVIDER,
marginRight: 10,
},
categoryTitle: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.CHARCOAL,
textTransform: "uppercase",
letterSpacing: 1,
},
pagesGrid: { flexDirection: "row", flexWrap: "wrap" },
pageCard: {
width: "48%",
marginRight: "2%",
marginBottom: 12,
padding: 10,
borderWidth: 1,
borderColor: COLORS.GRID,
backgroundColor: "#fafafa",
},
pageTitle: {
fontSize: FONT_SIZES.BODY,
fontWeight: "bold",
color: COLORS.TEXT_MAIN,
marginBottom: 4,
},
pageDesc: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_DIM,
lineHeight: 1.3,
},
});
export const SitemapModule = ({ state }: any) => (
<>
<DocumentTitle title="Informationsarchitektur" isHero={true} />
<PDFView style={styles.section}>
<PDFText style={styles.intro}>
Die folgende Struktur definiert die logische Hierarchie und
Benutzerführung. Sie dient als Bauplan für die technische Umsetzung und
stellt sicher, dass alle relevanten Geschäftsbereiche intuitiv
auffindbar sind.
</PDFText>
<PDFView style={styles.sitemapTree}>
<PDFView style={styles.rootNode}>
<PDFText style={styles.rootTitle}>Seitenstruktur</PDFText>
</PDFView>
{state.sitemap?.map((cat: any, i: number) => (
<PDFView key={i} style={styles.categorySection} wrap={false}>
<PDFView style={styles.categoryHeader}>
<PDFView style={styles.categoryIcon} />
<PDFText style={styles.categoryTitle}>{cat.category}</PDFText>
</PDFView>
<PDFView style={styles.pagesGrid}>
{cat.pages.map((p: any, j: number) => (
<PDFView
key={j}
style={[
styles.pageCard,
j % 2 === 1 ? { marginRight: 0 } : {},
]}
>
<PDFText style={styles.pageTitle}>{p.title}</PDFText>
{p.desc && (
<PDFText style={styles.pageDesc}>{p.desc}</PDFText>
)}
</PDFView>
))}
</PDFView>
</PDFView>
))}
</PDFView>
</PDFView>
</>
);

View File

@@ -1,224 +0,0 @@
import { FormState, Position, Totals } from "./types";
import {
FEATURE_LABELS,
FUNCTION_LABELS,
API_LABELS,
PAGE_LABELS,
} from "./constants";
export function calculateTotals(state: FormState, pricing: any): Totals {
if (state.projectType !== "website") {
return {
totalPrice: 0,
monthlyPrice: 0,
totalPagesCount: 0,
totalFeatures: 0,
totalFunctions: 0,
totalApis: 0,
languagesCount: 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 totalFeatures =
(state.features?.length || 0) +
(state.otherFeatures?.length || 0) +
(state.otherFeaturesCount || 0);
const totalFunctions =
(state.functions?.length || 0) +
(state.otherFunctions?.length || 0) +
(state.otherFunctionsCount || 0);
const totalApis =
(state.apiSystems?.length || 0) +
(state.otherTech?.length || 0) +
(state.otherTechCount || 0);
let total = pricing.BASE_WEBSITE;
total += totalPagesCount * pricing.PAGE;
total += totalFeatures * pricing.FEATURE;
total += totalFunctions * pricing.FUNCTION;
total += totalApis * pricing.API_INTEGRATION;
total += (state.newDatasets || 0) * pricing.NEW_DATASET;
if (state.cmsSetup) {
total += Math.max(1, totalFeatures) * pricing.CMS_CONNECTION_PER_FEATURE;
}
const languagesCount = state.languagesList?.length || 1;
if (languagesCount > 1) {
total *= 1 + (languagesCount - 1) * 0.2;
}
const monthlyPrice =
pricing.HOSTING_MONTHLY +
(state.storageExpansion || 0) * pricing.STORAGE_EXPANSION_MONTHLY;
return {
totalPrice: Math.round(total),
monthlyPrice: Math.round(monthlyPrice),
totalPagesCount,
totalFeatures,
totalFunctions,
totalApis,
languagesCount,
};
}
export function calculatePositions(state: FormState, pricing: any): Position[] {
const positions: Position[] = [];
let pos = 1;
if (state.projectType === "website") {
positions.push({
pos: pos++,
title: "Das technische Fundament",
desc: "Projekt-Setup, Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics.",
qty: 1,
price: pricing.BASE_WEBSITE,
});
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.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 (${uniquePages.join(", ")}).`,
qty: totalPagesCount,
price: totalPagesCount * pricing.PAGE,
});
if (state.features.length > 0 || (state.otherFeatures?.length || 0) > 0) {
const allFeatures = [
...state.features.map((f: string) => FEATURE_LABELS[f] || f),
...(state.otherFeatures || []),
];
positions.push({
pos: pos++,
title: "System-Module (Features)",
desc: `Implementierung funktionaler Bereiche: ${allFeatures.join(", ")}. Inklusive Datenstruktur und Darstellung.`,
qty: allFeatures.length,
price: allFeatures.length * pricing.FEATURE,
});
}
if (state.functions.length > 0 || (state.otherFunctions?.length || 0) > 0) {
const allFunctions = [
...state.functions.map((f: string) => FUNCTION_LABELS[f] || f),
...(state.otherFunctions || []),
];
positions.push({
pos: pos++,
title: "Logik-Funktionen",
desc: `Implementierung technischer Logik: ${allFunctions.join(", ")}.`,
qty: allFunctions.length,
price: allFunctions.length * pricing.FUNCTION,
});
}
if (state.apiSystems.length > 0 || (state.otherTech?.length || 0) > 0) {
const allApis = [
...state.apiSystems.map((a: string) => API_LABELS[a] || a),
...(state.otherTech || []),
];
positions.push({
pos: pos++,
title: "Schnittstellen (API)",
desc: `Anbindung externer Systeme zur Datensynchronisation: ${allApis.join(", ")}.`,
qty: allApis.length,
price: allApis.length * pricing.API_INTEGRATION,
});
}
if (state.cmsSetup) {
const totalFeatures =
state.features.length +
(state.otherFeatures?.length || 0) +
(state.otherFeaturesCount || 0);
const qty = Math.max(1, totalFeatures);
positions.push({
pos: pos++,
title: "Inhalts-Verwaltung",
desc: "Anbindung der System-Module an das Redaktions-System zur eigenständigen Pflege von Inhalten.",
qty: qty,
price: qty * pricing.CMS_CONNECTION_PER_FEATURE,
});
}
if (state.newDatasets > 0) {
positions.push({
pos: pos++,
title: "Inhaltliche Initial-Pflege",
desc: `Manuelle Übernahme und Aufbereitung von ${state.newDatasets} Datensätzen (Produkte, Artikel) in das Zielsystem.`,
qty: state.newDatasets,
price: state.newDatasets * pricing.NEW_DATASET,
});
}
const languagesCount = state.languagesList.length || 1;
if (languagesCount > 1) {
const subtotal = positions.reduce((sum, p) => sum + p.price, 0);
const factorPrice = subtotal * ((languagesCount - 1) * 0.2);
positions.push({
pos: pos++,
title: "Mehrsprachigkeit",
desc: `Erweiterung des Systems auf ${languagesCount} Sprachen (Struktur & Logik).`,
qty: languagesCount,
price: Math.round(factorPrice),
});
}
const monthlyRate =
pricing.HOSTING_MONTHLY +
state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY;
positions.push({
pos: pos++,
title: "Sorglos Betrieb (1 Jahr)",
desc: `Inklusive 1 Jahr Sicherung des technischen Betriebs, Hosting, Instandhaltung, Sicherheits-Updates und techn. Support gemäß AGB Punkt 7a.`,
qty: 1,
price: monthlyRate * 12,
});
} else {
positions.push({
pos: pos++,
title: "Web App / Software Entwicklung",
desc: "Individuelle Software-Entwicklung nach Aufwand. Abrechnung erfolgt auf Stundenbasis.",
qty: 1,
price: 0,
});
}
return positions;
}

View File

@@ -1,3 +1,2 @@
export * from './constants'; export * from "./constants";
export * from './calculator'; export * from "./types";
export * from './types';

View File

@@ -44,6 +44,7 @@
}, },
"dependencies": { "dependencies": {
"@directus/sdk": "21.0.0", "@directus/sdk": "21.0.0",
"@eslint/compat": "^2.0.2" "@eslint/compat": "^2.0.2",
"@mintel/acquisition": "link:../at-mintel/packages/acquisition-library"
} }
} }

84
pnpm-lock.yaml generated
View File

@@ -18,6 +18,9 @@ importers:
"@eslint/compat": "@eslint/compat":
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.2(eslint@10.0.0(jiti@1.21.7)) version: 2.0.2(eslint@10.0.0(jiti@1.21.7))
"@mintel/acquisition":
specifier: link:../at-mintel/packages/acquisition-library
version: link:../at-mintel/packages/acquisition-library
devDependencies: devDependencies:
"@eslint/eslintrc": "@eslint/eslintrc":
specifier: ^3.3.3 specifier: ^3.3.3
@@ -30,7 +33,7 @@ importers:
version: 1.7.8 version: 1.7.8
"@mintel/eslint-config": "@mintel/eslint-config":
specifier: ^1.7.3 specifier: ^1.7.3
version: 1.7.8(@typescript-eslint/parser@8.54.0(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) version: 1.7.8(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)
"@mintel/husky-config": "@mintel/husky-config":
specifier: ^1.7.3 specifier: ^1.7.3
version: 1.7.8 version: 1.7.8
@@ -91,6 +94,9 @@ importers:
"@directus/sdk": "@directus/sdk":
specifier: 21.0.0 specifier: 21.0.0
version: 21.0.0 version: 21.0.0
"@mintel/pdf":
specifier: link:../../../at-mintel/packages/pdf-library
version: link:../../../at-mintel/packages/pdf-library
"@opentelemetry/api": "@opentelemetry/api":
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
@@ -126,7 +132,7 @@ importers:
version: 10.38.0 version: 10.38.0
"@sentry/nextjs": "@sentry/nextjs":
specifier: 10.38.0 specifier: 10.38.0
version: 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1) version: 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1)
"@types/canvas-confetti": "@types/canvas-confetti":
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
@@ -13317,15 +13323,32 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
"@mintel/eslint-config@1.7.8(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)":
dependencies:
"@eslint/eslintrc": 3.3.3
"@eslint/js": 9.39.2
"@next/eslint-plugin-next": 16.1.6
eslint-config-next: 16.1.6(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)
eslint-plugin-react: 7.37.5(eslint@10.0.0(jiti@1.21.7))
eslint-plugin-react-hooks: 7.0.1(eslint@10.0.0(jiti@1.21.7))
typescript-eslint: 8.54.0(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)
transitivePeerDependencies:
- "@typescript-eslint/parser"
- eslint
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
- typescript
"@mintel/husky-config@1.7.8": "@mintel/husky-config@1.7.8":
dependencies: dependencies:
"@commitlint/config-conventional": 20.4.1 "@commitlint/config-conventional": 20.4.1
"@mintel/next-config@1.7.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.96.1)": "@mintel/next-config@1.7.8(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.96.1)":
dependencies: dependencies:
"@sentry/nextjs": 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1) "@sentry/nextjs": 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1)
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- "@babel/core" - "@babel/core"
- "@opentelemetry/api" - "@opentelemetry/api"
@@ -13348,7 +13371,7 @@ snapshots:
dependencies: dependencies:
"@directus/sdk": 21.0.0 "@directus/sdk": 21.0.0
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) next-intl: 4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
zod: 3.22.3 zod: 3.22.3
transitivePeerDependencies: transitivePeerDependencies:
- "@babel/core" - "@babel/core"
@@ -14276,7 +14299,7 @@ snapshots:
"@sentry/utils": 7.120.4 "@sentry/utils": 7.120.4
localforage: 1.10.0 localforage: 1.10.0
"@sentry/nextjs@10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1)": "@sentry/nextjs@10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.96.1)":
dependencies: dependencies:
"@opentelemetry/api": 1.9.0 "@opentelemetry/api": 1.9.0
"@opentelemetry/semantic-conventions": 1.39.0 "@opentelemetry/semantic-conventions": 1.39.0
@@ -16402,6 +16425,26 @@ snapshots:
- eslint-plugin-import-x - eslint-plugin-import-x
- supports-color - supports-color
eslint-config-next@16.1.6(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3):
dependencies:
"@next/eslint-plugin-next": 16.1.6
eslint: 10.0.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@10.0.0(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.0(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.0(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@10.0.0(jiti@1.21.7))
eslint-plugin-react-hooks: 7.0.1(eslint@10.0.0(jiti@1.21.7))
globals: 16.4.0
typescript-eslint: 8.54.0(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- "@typescript-eslint/parser"
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
eslint-import-resolver-node@0.3.9: eslint-import-resolver-node@0.3.9:
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
@@ -16465,6 +16508,33 @@ snapshots:
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
- supports-color - supports-color
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.0(jiti@1.21.7)):
dependencies:
"@rtsao/scc": 1.1.0
array-includes: 3.1.9
array.prototype.findlastindex: 1.2.6
array.prototype.flat: 1.3.3
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 10.0.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@10.0.0(jiti@1.21.7))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.1
semver: 6.3.1
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@10.0.0(jiti@1.21.7)): eslint-plugin-jsx-a11y@6.10.2(eslint@10.0.0(jiti@1.21.7)):
dependencies: dependencies:
aria-query: 5.3.2 aria-query: 5.3.2
@@ -18003,7 +18073,7 @@ snapshots:
next-intl-swc-plugin-extractor@4.8.2: {} next-intl-swc-plugin-extractor@4.8.2: {}
next-intl@4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3): next-intl@4.8.2(@swc/helpers@0.5.18)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
dependencies: dependencies:
"@formatjs/intl-localematcher": 0.5.10 "@formatjs/intl-localematcher": 0.5.10
"@parcel/watcher": 2.5.6 "@parcel/watcher": 2.5.6