diff --git a/din_test_final.pdf b/din_test_final.pdf new file mode 100644 index 0000000..7846872 Binary files /dev/null and b/din_test_final.pdf differ diff --git a/din_test_final_v2.pdf b/din_test_final_v2.pdf new file mode 100644 index 0000000..5b932cd Binary files /dev/null and b/din_test_final_v2.pdf differ diff --git a/din_test_v2.pdf b/din_test_v2.pdf new file mode 100644 index 0000000..ed976de Binary files /dev/null and b/din_test_v2.pdf differ diff --git a/package.json b/package.json index f129ebb..aacdb68 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts", "clone-website": "tsx ./scripts/clone-recursive.ts", "clone-page": "tsx ./scripts/clone-page.ts", + "generate-quote": "tsx ./scripts/generate-quote.ts", "video:preview": "remotion preview video/index.ts", "video:render": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4", "video:render:contact": "remotion render video/index.ts ContactFormShowcase out/contact-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite", @@ -65,4 +66,4 @@ "tsx": "^4.21.0", "typescript": "5.9.3" } -} +} \ No newline at end of file diff --git a/scripts/generate-quote.ts b/scripts/generate-quote.ts new file mode 100644 index 0000000..ed85ed6 --- /dev/null +++ b/scripts/generate-quote.ts @@ -0,0 +1,167 @@ +import * as React from 'react'; +import { renderToFile } from '@react-pdf/renderer'; +import { EstimationPDF } from '../src/components/EstimationPDF'; +import { + PRICING, + initialState, + PAGE_SAMPLES, + FEATURE_OPTIONS, + FUNCTION_OPTIONS, + API_OPTIONS, + DESIGN_OPTIONS, + DEADLINE_LABELS, + ASSET_OPTIONS, + EMPLOYEE_OPTIONS, + calculatePositions, + FormState +} from '../src/logic/pricing'; +import * as fs from 'fs'; +import * as path from 'path'; +import { parseArgs } from 'util'; +import * as readline from 'readline/promises'; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function ask(question: string, defaultValue?: string): Promise { + const answer = await rl.question(`${question}${defaultValue ? ` [${defaultValue}]` : ''}: `); + return answer.trim() || defaultValue || ''; +} + +async function selectOne(question: string, options: { id: string; label: string }[], defaultValue?: string): Promise { + console.log(`\n${question}`); + options.forEach((opt, i) => console.log(`${i + 1}. ${opt.label}`)); + const answer = await ask('Select number'); + const index = parseInt(answer) - 1; + return options[index]?.id || defaultValue || options[0].id; +} + +async function selectMany(question: string, options: { id: string; label: string }[]): Promise { + console.log(`\n${question} (comma separated numbers, e.g. 1,2,4)`); + options.forEach((opt, i) => console.log(`${i + 1}. ${opt.label}`)); + const answer = await ask('Selection'); + if (!answer) return []; + return answer.split(',') + .map(n => parseInt(n.trim()) - 1) + .filter(i => options[i]) + .map(i => options[i].id); +} + +async function generate() { + const { values } = parseArgs({ + options: { + input: { type: 'string', short: 'i' }, + output: { type: 'string', short: 'o' }, + interactive: { type: 'boolean', short: 'I', default: false }, + name: { type: 'string' }, + company: { type: 'string' }, + email: { type: 'string' }, + 'project-type': { type: 'string' }, + }, + }); + + let state: FormState = { ...initialState }; + + // Apply command line flags first + if (values.name) state.name = values.name as string; + if (values.company) state.companyName = values.company as string; + if (values.email) state.email = values.email as string; + if (values['project-type']) state.projectType = values['project-type'] as 'website' | 'web-app'; + + if (values.input) { + const inputPath = path.resolve(process.cwd(), values.input); + if (fs.existsSync(inputPath)) { + const fileData = JSON.parse(fs.readFileSync(inputPath, 'utf-8')); + state = { ...state, ...fileData }; + } + } + + if (values.interactive) { + console.log('--- Mintel Quote Generator Wizard ---'); + + state.name = await ask('Client Name', state.name); + state.companyName = await ask('Company Name', state.companyName); + state.email = await ask('Client Email', state.email); + + state.projectType = (await selectOne('Project Type', [ + { id: 'website', label: 'Website' }, + { id: 'web-app', label: 'Web App' } + ], state.projectType)) as 'website' | 'web-app'; + + if (state.projectType === 'website') { + state.websiteTopic = await ask('Website Topic', state.websiteTopic); + state.selectedPages = await selectMany('Pages', PAGE_SAMPLES); + state.features = await selectMany('Features', FEATURE_OPTIONS); + state.functions = await selectMany('Functions', FUNCTION_OPTIONS); + state.apiSystems = await selectMany('APIs', API_OPTIONS); + + const cms = await ask('CMS Setup? (y/n)', state.cmsSetup ? 'y' : 'n'); + state.cmsSetup = cms.toLowerCase() === 'y'; + + const langs = await ask('Languages (comma separated)', state.languagesList.join(', ')); + state.languagesList = langs.split(',').map(l => l.trim()).filter(Boolean); + } else { + state.targetAudience = await selectOne('Target Audience', [ + { id: 'internal', label: 'Internal Tool' }, + { id: 'customers', label: 'Customer Portal' } + ], state.targetAudience); + + state.platformType = await selectOne('Platform', [ + { id: 'web-only', label: 'Web Only' }, + { id: 'cross-platform', label: 'Cross Platform' } + ], state.platformType); + + state.dataSensitivity = await selectOne('Data Sensitivity', [ + { id: 'standard', label: 'Standard' }, + { id: 'high', label: 'High / Sensitive' } + ], state.dataSensitivity); + } + + state.employeeCount = await selectOne('Company Size', EMPLOYEE_OPTIONS, state.employeeCount); + state.designVibe = await selectOne('Design Vibe', DESIGN_OPTIONS, state.designVibe); + state.deadline = await selectOne('Timeline', Object.entries(DEADLINE_LABELS).map(([id, label]) => ({ id, label })), state.deadline); + state.assets = await selectMany('Available Assets', ASSET_OPTIONS); + + state.message = await ask('Additional Message / Remarks', state.message); + } + + rl.close(); + + const totalPagesCount = state.selectedPages.length + state.otherPages.length + (state.otherPagesCount || 0); + const positions = calculatePositions(state, PRICING); + const totalPrice = positions.reduce((sum, p) => sum + p.price, 0); + const monthlyPrice = PRICING.HOSTING_MONTHLY + (state.storageExpansion * PRICING.STORAGE_EXPANSION_MONTHLY); + + const outputPath = values.output + ? path.resolve(process.cwd(), values.output) + : path.resolve(process.cwd(), `quote_${state.companyName.replace(/\s+/g, '_') || 'client'}_${new Date().toISOString().split('T')[0]}.pdf`); + + console.log(`\nGenerating PDF for ${state.companyName || state.name || 'Unknown Client'}...`); + + const headerIcon = path.resolve(process.cwd(), 'src/assets/logo/Icon White Transparent.png'); + const footerLogo = path.resolve(process.cwd(), 'src/assets/logo/Logo Black Transparent.png'); + + try { + // @ts-ignore + await renderToFile( + React.createElement(EstimationPDF, { + state, + totalPrice, + monthlyPrice, + totalPagesCount, + pricing: PRICING, + headerIcon, + footerLogo, + }), + outputPath + ); + console.log(`Successfully generated: ${outputPath}`); + } catch (error) { + console.error('Error generating PDF:', error); + process.exit(1); + } +} + +generate(); diff --git a/src/components/ContactForm/components/PriceCalculation.tsx b/src/components/ContactForm/components/PriceCalculation.tsx index e3fcf8d..2402cbe 100644 --- a/src/components/ContactForm/components/PriceCalculation.tsx +++ b/src/components/ContactForm/components/PriceCalculation.tsx @@ -9,6 +9,8 @@ import { Info, Download, Share2, RefreshCw } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import dynamic from 'next/dynamic'; import { EstimationPDF } from '../../EstimationPDF'; +import IconWhite from '../../../assets/logo/Icon White Transparent.png'; +import LogoBlack from '../../../assets/logo/Logo Black Transparent.png'; // Dynamically import PDF components to avoid SSR issues const PDFDownloadLink = dynamic( @@ -26,12 +28,12 @@ interface PriceCalculationProps { onShare?: () => void; } -export function PriceCalculation({ - state, - totalPrice, - monthlyPrice, - totalPagesCount, - isClient, +export function PriceCalculation({ + state, + totalPrice, + monthlyPrice, + totalPagesCount, + isClient, qrCodeData, onShare }: PriceCalculationProps) { @@ -45,11 +47,12 @@ export function PriceCalculation({ const handleDownload = async () => { if (pdfLoading) return; - + setPdfLoading(true); - + try { const { pdf } = await import('@react-pdf/renderer'); + const doc = ; - + // Minimum loading time of 2 seconds for better UX const [blob] = await Promise.all([ pdf(doc).toBlob(), new Promise(resolve => setTimeout(resolve, 2000)) ]); - + const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -111,7 +116,7 @@ export function PriceCalculation({ {monthlyPrice.toLocaleString()} € / Monat - +
{isClient && (