diff --git a/apps/sample-website/Dockerfile b/apps/sample-website/Dockerfile index 77e2209..41c3342 100644 --- a/apps/sample-website/Dockerfile +++ b/apps/sample-website/Dockerfile @@ -1,5 +1,6 @@ # Start from the pre-built Nextjs Base image -FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder +ARG IMAGE_TAG=latest +FROM registry.infra.mintel.me/mintel/nextjs:${IMAGE_TAG} AS builder WORKDIR /app @@ -20,7 +21,7 @@ ENV DIRECTUS_URL=$DIRECTUS_URL RUN pnpm --filter sample-website build # Production runner image -FROM registry.infra.mintel.me/mintel/runtime:latest AS runner +FROM registry.infra.mintel.me/mintel/runtime:${IMAGE_TAG} AS runner WORKDIR /app COPY --from=builder /app/apps/sample-website/public ./apps/sample-website/public diff --git a/packages/acquisition/package.json b/packages/acquisition/package.json new file mode 100644 index 0000000..0d05cb3 --- /dev/null +++ b/packages/acquisition/package.json @@ -0,0 +1,32 @@ +{ + "name": "@mintel/acquisition", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsup src/index.ts --format esm --dts --clean", + "dev": "tsup src/index.ts --format esm --watch --dts", + "lint": "eslint src", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "axios": "^1.7.9", + "crawlee": "^3.12.2", + "cheerio": "^1.0.0", + "react": "^19.0.0", + "@react-pdf/renderer": "^4.3.0", + "framer-motion": "^12.4.2" + }, + "devDependencies": { + "@mintel/tsconfig": "workspace:*", + "@mintel/eslint-config": "workspace:*", + "tsup": "^8.3.5", + "typescript": "^5.0.0", + "vitest": "^3.0.4", + "@types/node": "^20.17.16" + } +} \ No newline at end of file diff --git a/packages/acquisition/src/components/EstimationPDF.tsx b/packages/acquisition/src/components/EstimationPDF.tsx new file mode 100644 index 0000000..9fd4d22 --- /dev/null +++ b/packages/acquisition/src/components/EstimationPDF.tsx @@ -0,0 +1,93 @@ +"use client"; + +import * as React from "react"; +import { Page as PDFPage, Document as PDFDocument } from "@react-pdf/renderer"; +import { pdfStyles } from "./pdf/SharedUI.js"; +import { SimpleLayout } from "./pdf/SimpleLayout.js"; + +// Modules +import { FrontPageModule } from "./pdf/modules/FrontPageModule.js"; +import { BriefingModule } from "./pdf/modules/BriefingModule.js"; +import { SitemapModule } from "./pdf/modules/SitemapModule.js"; +import { EstimationModule } from "./pdf/modules/EstimationModule.js"; +import { TransparenzModule } from "./pdf/modules/TransparenzModule.js"; +import { ClosingModule } from "./pdf/modules/CommonModules.js"; + +import { calculatePositions } from "../logic/pricing/calculator.js"; + +interface PDFProps { + state: any; + totalPrice: number; + pricing: any; + headerIcon?: string; + footerLogo?: string; +} + +export const EstimationPDF = ({ + state, + totalPrice, + pricing, + headerIcon, + footerLogo, +}: 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 commonProps = { + state, + date, + icon: headerIcon, + footerLogo, + companyData, + }; + + let pageCounter = 1; + const getPageNum = () => (pageCounter++).toString().padStart(2, "0"); + + return ( + + + + + + + + + + {state.sitemap && state.sitemap.length > 0 && ( + + + + )} + + + + + + + + + + + + + + ); +}; diff --git a/packages/acquisition/src/components/pdf/SharedUI.tsx b/packages/acquisition/src/components/pdf/SharedUI.tsx new file mode 100644 index 0000000..b0ceada --- /dev/null +++ b/packages/acquisition/src/components/pdf/SharedUI.tsx @@ -0,0 +1,401 @@ +"use client"; + +import * as React from "react"; +import { + View as PDFView, + Text as PDFText, + StyleSheet, + Image as PDFImage, +} 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 +}; + +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, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + marginBottom: 20, + minHeight: 120, + }, + addressBlock: { + width: "55%", + marginTop: 45, + }, + 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, + }, + 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, + }, + 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, + }, +}); + +export const IndustrialListItem = ({ + children, +}: { + children: React.ReactNode; +}) => ( + + + {children} + +); + +export const Divider = ({ style = {} }: { style?: any }) => ( + +); + +export const Footer = ({ + logo, + companyData, + showDetails = true, + showPageNumber = true, +}: { + logo?: string; + companyData: any; + showDetails?: boolean; + showPageNumber?: boolean; +}) => ( + + + {logo ? ( + + ) : ( + + marc mintel + + )} + + {showDetails && ( + <> + + + {companyData.name} + {"\n"} + {companyData.address1} + {"\n"} + {companyData.address2} + {"\n"}UST: {companyData.ustId} + + + + {showPageNumber && ( + + `${pageNumber} / ${totalPages}` + } + fixed + /> + )} + + + )} + +); + +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; +}) => ( + + + {showAddress && sender && ( + <> + {sender} + {recipient && ( + + + {recipient.title} + + {recipient.subtitle && {recipient.subtitle}} + {recipient.address && {recipient.address}} + {recipient.phone && {recipient.phone}} + {recipient.email && {recipient.email}} + {recipient.taxId && USt-ID: {recipient.taxId}} + + )} + + )} + + + + {icon ? ( + + ) : ( + M + )} + + + +); + +export const DocumentTitle = ({ + title, + subLines, + isHero = false, +}: { + title: string; + subLines?: string[]; + isHero?: boolean; +}) => ( + + + {title} + + {subLines?.map((line, i) => ( + + {line} + + ))} + +); + +export const TechnicalSpec = ({ + label, + value, +}: { + label: string; + value: string; +}) => ( + + {label} + {value} + +); + +export const AsymmetryView = ({ + left, + right, + style = {}, +}: { + left: React.ReactNode; + right: React.ReactNode; + style?: any; +}) => ( + + {left} + {right} + +); diff --git a/packages/acquisition/src/components/pdf/SimpleLayout.tsx b/packages/acquisition/src/components/pdf/SimpleLayout.tsx new file mode 100644 index 0000000..4640196 --- /dev/null +++ b/packages/acquisition/src/components/pdf/SimpleLayout.tsx @@ -0,0 +1,64 @@ +'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 } from './SharedUI.js'; + +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; + showPageNumber?: boolean; +} + +export const SimpleLayout = ({ + children, + pageNumber, + icon, + footerLogo, + companyData, + showPageNumber = true +}: SimpleLayoutProps) => { + return ( + +
+ {pageNumber && {pageNumber}} + + + {children} + + +