diff --git a/src/components/marketing/ReprintSavingsCalculator.tsx b/src/components/marketing/ReprintSavingsCalculator.tsx index d28b647..c24475e 100644 --- a/src/components/marketing/ReprintSavingsCalculator.tsx +++ b/src/components/marketing/ReprintSavingsCalculator.tsx @@ -38,14 +38,26 @@ export const ReprintSavingsCalculator: React.FC = () => { const annualWaste = cost * updates; const withQRMaster = qrMasterYearlyCost; const savings = annualWaste - withQRMaster; - const savingsPercent = Math.round((savings / annualWaste) * 100); - const monthsPaidFor = Math.round(annualWaste / (qrMasterYearlyCost / 12)); + + // Calculate percentages based on the larger value to avoid overflow bars + const maxValue = Math.max(annualWaste, withQRMaster); + const wastePercentOfMax = (annualWaste / maxValue) * 100; + const costPercentOfMax = (withQRMaster / maxValue) * 100; + + const isPositiveSavings = savings > 0; + const savingsPercent = isPositiveSavings + ? Math.round((savings / annualWaste) * 100) + : Math.round((Math.abs(savings) / withQRMaster) * 100); + + const monthsPaidFor = isPositiveSavings + ? Math.round(annualWaste / (qrMasterYearlyCost / 12)) + : 0; return { annualWaste, withQRMaster, - savings: Math.max(0, savings), - savingsPercent: Math.max(0, savingsPercent), + savings, + savingsPercent, monthsPaidFor, }; }, [reprintCost, updatesPerYear]); @@ -120,6 +132,8 @@ export const ReprintSavingsCalculator: React.FC = () => { }).format(value); }; + const isPositiveSavings = results ? results.savings > 0 : false; + return ( <>
@@ -243,21 +257,44 @@ export const ReprintSavingsCalculator: React.FC = () => { className="h-full flex flex-col" >
-
- - Potential Savings Found -
+ {isPositiveSavings ? ( +
+ + Potential Savings Found +
+ ) : ( +
+ + Cost Analysis +
+ )}
-

Annual Waste Identified

-
- - {formatCurrency(results.annualWaste)} - -
-

- Money currently lost to static reprints -

+ {isPositiveSavings ? ( + <> +

Annual Waste Identified

+
+ + {formatCurrency(results.annualWaste)} + +
+

+ Money currently lost to static reprints +

+ + ) : ( + <> +

Estimated Annual Investment

+
+ + {formatCurrency(Math.abs(results.savings))} + +
+

+ Difference for upgrading to Dynamic QR features +

+ + )}
{/* Comparison Bars */} @@ -270,7 +307,7 @@ export const ReprintSavingsCalculator: React.FC = () => {
@@ -280,35 +317,53 @@ export const ReprintSavingsCalculator: React.FC = () => {
QR Master Cost - {formatCurrency(results.withQRMaster)} + {formatCurrency(results.withQRMaster)}
-
-
-
- -
-
-

- You save {results.savingsPercent}% instantly -

-

- That's {formatCurrency(results.savings)} extra budget per year. - Effectively getting {results.monthsPaidFor} months of service for free compared to just one single reprint. -

+ {isPositiveSavings ? ( +
+
+
+ +
+
+

+ You save {results.savingsPercent}% instantly +

+

+ That's {formatCurrency(results.savings)} extra budget per year. + Effectively getting {results.monthsPaidFor} months of service for free compared to just one single reprint. +

+
-
+ ) : ( +
+
+
+ +
+
+

+ Premium features for just {formatCurrency(Math.abs(results.savings))}/year +

+

+ For a small investment, you gain full control, analytics, and the ability to update destinations anytime. +

+
+
+
+ )}
@@ -319,10 +374,10 @@ export const ReprintSavingsCalculator: React.FC = () => {

Based on QR Master Pro annual plan. No credit card required to start. diff --git a/src/lib/generateSavingsReport.ts b/src/lib/generateSavingsReport.ts index 4a1070e..72b54a6 100644 --- a/src/lib/generateSavingsReport.ts +++ b/src/lib/generateSavingsReport.ts @@ -12,131 +12,309 @@ interface ReportData { export function generateSavingsReport(data: ReportData): void { const doc = new jsPDF(); const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const margin = 20; - // Colors - const primaryColor: [number, number, number] = [79, 70, 229]; // Indigo - const greenColor: [number, number, number] = [16, 185, 129]; // Emerald - const redColor: [number, number, number] = [239, 68, 68]; // Red - const grayColor: [number, number, number] = [100, 116, 139]; // Slate + // Brand Colors + const colors = { + primary: [79, 70, 229] as [number, number, number], // Indigo 600 + primaryLight: [224, 231, 255] as [number, number, number], // Indigo 100 + success: [16, 185, 129] as [number, number, number], // Emerald 500 + successBg: [236, 253, 245] as [number, number, number], // Emerald 50 + text: [30, 41, 59] as [number, number, number], // Slate 800 + textLight: [100, 116, 139] as [number, number, number], // Slate 500 + border: [226, 232, 240] as [number, number, number], // Slate 200 + danger: [239, 68, 68] as [number, number, number], // Red 500 + dangerBg: [254, 242, 242] as [number, number, number], // Red 50 + }; - // Header section with brand color - doc.setFillColor(...primaryColor); - doc.rect(0, 0, pageWidth, 45, 'F'); + let currentY = 20; - // Logo text - doc.setTextColor(255, 255, 255); - doc.setFontSize(28); - doc.setFont('helvetica', 'bold'); - doc.text('QR Master', 20, 28); - - // Subtitle - doc.setFontSize(12); - doc.setFont('helvetica', 'normal'); - doc.text('Your Personalized Savings Report', 20, 38); - - // Main title - doc.setTextColor(30, 41, 59); - doc.setFontSize(22); - doc.setFont('helvetica', 'bold'); - doc.text('Reprint Cost Analysis', 20, 65); - - // Date - doc.setTextColor(...grayColor); - doc.setFontSize(10); - doc.setFont('helvetica', 'normal'); - doc.text(`Generated on ${new Date().toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - })}`, 20, 75); - - // Input summary box - doc.setFillColor(248, 250, 252); - doc.roundedRect(20, 85, pageWidth - 40, 45, 3, 3, 'F'); - - doc.setTextColor(30, 41, 59); - doc.setFontSize(12); - doc.setFont('helvetica', 'bold'); - doc.text('Your Input', 28, 98); - - doc.setFont('helvetica', 'normal'); - doc.setFontSize(11); - doc.setTextColor(...grayColor); - doc.text(`Reprint Cost per Batch:`, 28, 112); - doc.text(`URL Updates per Year:`, 28, 122); - - doc.setTextColor(30, 41, 59); - doc.setFont('helvetica', 'bold'); - doc.text(`€${data.reprintCost.toLocaleString()}`, 100, 112); - doc.text(`${data.updatesPerYear}`, 100, 122); - - // Results section - doc.setTextColor(30, 41, 59); - doc.setFontSize(14); - doc.setFont('helvetica', 'bold'); - doc.text('Cost Comparison', 20, 150); - - // Static QR Code cost (red) - doc.setFillColor(254, 242, 242); - doc.roundedRect(20, 158, pageWidth - 40, 28, 3, 3, 'F'); - - doc.setTextColor(...redColor); - doc.setFontSize(11); - doc.setFont('helvetica', 'bold'); - doc.text('Static QR Codes (Annual Waste)', 28, 172); - doc.setFontSize(16); - doc.text(`€${data.annualWaste.toLocaleString()}`, pageWidth - 28, 172, { align: 'right' }); - - // QR Master cost (green) - doc.setFillColor(236, 253, 245); - doc.roundedRect(20, 192, pageWidth - 40, 28, 3, 3, 'F'); - - doc.setTextColor(...greenColor); - doc.setFontSize(11); - doc.setFont('helvetica', 'bold'); - doc.text('QR Master Pro (Annual Cost)', 28, 206); - doc.setFontSize(16); - doc.text(`€${data.qrMasterCost.toLocaleString()}`, pageWidth - 28, 206, { align: 'right' }); - - // Savings highlight - doc.setFillColor(...greenColor); - doc.roundedRect(20, 230, pageWidth - 40, 40, 3, 3, 'F'); - - doc.setTextColor(255, 255, 255); - doc.setFontSize(12); - doc.setFont('helvetica', 'normal'); - doc.text('Your Annual Savings', 28, 245); + // --- Header --- + // Logo / Brand Name + doc.setTextColor(...colors.primary); doc.setFontSize(24); doc.setFont('helvetica', 'bold'); - doc.text(`€${data.savings.toLocaleString()}`, 28, 262); - doc.setFontSize(14); - doc.text(`(${data.savingsPercent}%)`, 90, 262); + doc.text('QR Master', margin, currentY + 8); - // CTA section - doc.setTextColor(30, 41, 59); - doc.setFontSize(14); - doc.setFont('helvetica', 'bold'); - doc.text('Ready to Start Saving?', 20, 295); - - doc.setTextColor(...grayColor); + // Date (Right aligned) + doc.setTextColor(...colors.textLight); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); - doc.text('Dynamic QR codes let you update destinations without reprinting.', 20, 307); - doc.text('Track scans, analyze performance, and never waste print budget again.', 20, 317); + const dateStr = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); + doc.text(dateStr, pageWidth - margin, currentY + 8, { align: 'right' }); - // Website link - doc.setTextColor(...primaryColor); + currentY += 20; + + // Title Section + doc.setLineWidth(0.5); + doc.setDrawColor(...colors.primary); + doc.line(margin, currentY, margin + 40, currentY); // Small accent line + + currentY += 10; + + doc.setTextColor(...colors.text); + doc.setFontSize(28); doc.setFont('helvetica', 'bold'); - doc.text('Start your free trial: www.qrmaster.net/signup', 20, 332); + doc.text('Savings Analysis', margin, currentY); - // Footer - doc.setTextColor(...grayColor); - doc.setFontSize(8); + currentY += 8; + doc.setTextColor(...colors.textLight); + doc.setFontSize(12); doc.setFont('helvetica', 'normal'); - const footerY = doc.internal.pageSize.getHeight() - 15; - doc.text('© 2026 QR Master. All rights reserved.', pageWidth / 2, footerY, { align: 'center' }); - doc.text('www.qrmaster.net', pageWidth / 2, footerY + 8, { align: 'center' }); + doc.text('Prepared specifically for your business based on your input parameters.', margin, currentY); - // Download the PDF - doc.save('QRMaster-Savings-Report.pdf'); + currentY += 20; + + // --- Input Summary --- + // Background for inputs + doc.setFillColor(248, 250, 252); // Slate 50 + doc.setDrawColor(...colors.border); + doc.roundedRect(margin, currentY, pageWidth - (margin * 2), 35, 2, 2, 'FD'); + + const inputInnerY = currentY + 12; + + // Label 1 + doc.setFontSize(10); + doc.setTextColor(...colors.textLight); + doc.text('REPRINT COST PER BATCH', margin + 10, inputInnerY); + + // Value 1 + doc.setFontSize(16); + doc.setTextColor(...colors.text); + doc.setFont('helvetica', 'bold'); + doc.text(`€${data.reprintCost.toLocaleString()}`, margin + 10, inputInnerY + 10); + + // Divider + doc.setDrawColor(...colors.border); + doc.line(pageWidth / 2, inputInnerY - 5, pageWidth / 2, inputInnerY + 18); + + // Label 2 + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.setTextColor(...colors.textLight); + doc.text('URL UPDATES/YEAR', (pageWidth / 2) + 10, inputInnerY); + + // Value 2 + doc.setFontSize(16); + doc.setTextColor(...colors.text); + doc.setFont('helvetica', 'bold'); + doc.text(`${data.updatesPerYear}`, (pageWidth / 2) + 10, inputInnerY + 10); + + currentY += 50; + + // --- Analysis Results --- + + doc.setFontSize(14); + doc.setTextColor(...colors.text); + doc.setFont('helvetica', 'bold'); + doc.text('Cost Analysis', margin, currentY); + currentY += 10; + + // 1. Static Codes (Bad) + const cardHeight = 26; // Reduced height + // perfect vertical centering: (Height 26 / 2) + (Approx cap-height correction ~3) = 16 + const textCenterOffset = 16; + + // Draw "Static" Row + doc.setDrawColor(...colors.border); + doc.setFillColor(255, 255, 255); + doc.roundedRect(margin, currentY, pageWidth - (margin * 2), cardHeight, 2, 2, 'FD'); + + // Icon/Label + doc.setTextColor(...colors.danger); + doc.setFontSize(11); + doc.setFont('helvetica', 'bold'); + doc.text('• Static QR Codes', margin + 8, currentY + textCenterOffset); + + doc.setTextColor(...colors.textLight); + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + // Align sub-label relative to first label + doc.text('(Print & Reprint Costs)', margin + 50, currentY + textCenterOffset); + + doc.setTextColor(...colors.text); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(`€${data.annualWaste.toLocaleString()}`, pageWidth - margin - 10, currentY + textCenterOffset, { align: 'right' }); + + currentY += cardHeight + 4; + + // Draw "QR Master" Row + doc.setDrawColor(...colors.border); + doc.setFillColor(255, 255, 255); + doc.roundedRect(margin, currentY, pageWidth - (margin * 2), cardHeight, 2, 2, 'FD'); + + // Icon/Label + doc.setTextColor(...colors.success); + doc.setFontSize(11); + doc.setFont('helvetica', 'bold'); + doc.text('• QR Master Pro', margin + 8, currentY + textCenterOffset); + + doc.setTextColor(...colors.textLight); + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text('(Subscription)', margin + 50, currentY + textCenterOffset); + + doc.setTextColor(...colors.text); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(`€${data.qrMasterCost.toLocaleString()}`, pageWidth - margin - 10, currentY + textCenterOffset, { align: 'right' }); + + currentY += cardHeight + 15; + + // --- PAGE CHANGE: Results & Strategy on Page 2 --- + doc.addPage(); + currentY = 40; + + // --- TOTAL SAVINGS / INVESTMENT HERO SECTION --- + // (No line divider needed at top of new page) + + const isPositiveSavings = data.savings > 0; + const heroColor = isPositiveSavings ? colors.success : colors.primary; // Green or Indigo + const heroBg = isPositiveSavings ? colors.successBg : colors.primaryLight; + + doc.setTextColor(...colors.textLight); + doc.setFontSize(11); + doc.setFont('helvetica', 'bold'); + + const heroTitle = isPositiveSavings ? 'PROJECTED ANNUAL SAVINGS' : 'ANNUAL INVESTMENT REQUIRED'; + doc.text(heroTitle, margin, currentY); + + // Big Number and Badge Container + const savingsY = currentY + 12; + + // 1. The Amount + doc.setTextColor(...heroColor); + doc.setFontSize(42); + doc.setFont('helvetica', 'bold'); + const amountText = `€${Math.abs(data.savings).toLocaleString()}`; + doc.text(amountText, margin, savingsY + 12); + + // 2. The Percentage Badge (Centered relative to the number) + const numberWidth = doc.getStringUnitWidth(amountText) * 42 / doc.internal.scaleFactor; + const badgeX = margin + numberWidth + 10; // Slightly tighter gap + + // Determine badge text and dynamic width + doc.setFontSize(9); + doc.setFont('helvetica', 'bold'); + const badgeText = isPositiveSavings ? `${data.savingsPercent}% Saved` : 'Upgrade'; + const textWidth = doc.getStringUnitWidth(badgeText) * 9 / doc.internal.scaleFactor; + + const badgePadding = 12; // 6px on each side + const badgeWidth = textWidth + badgePadding; + const badgeHeight = 14; // Reduced height (was 16) + + // Draw Compact Badge + doc.setFillColor(...heroBg); + doc.setDrawColor(...heroColor); + doc.setLineWidth(0.5); + doc.roundedRect(badgeX, savingsY + 1, badgeWidth, badgeHeight, 6, 6, 'FD'); // y+1 to align better with text baseline + + doc.setTextColor(...heroColor); + doc.text(badgeText, badgeX + (badgeWidth / 2), savingsY + 10, { align: 'center' }); // Centered + + currentY += 40; // Reduced gap + + // --- Recommended Resources --- + doc.setTextColor(...colors.text); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Recommended Reading', margin, currentY); + currentY += 8; + + const resources = [ + { title: "The ROI of Dynamic QR Codes", url: "www.qrmaster.net/blog/roi-dynamic-qr" }, + { title: "How to Track Physical Marketing", url: "www.qrmaster.net/blog/tracking-guide" }, + { title: "Best Practices for QR Campaigns", url: "www.qrmaster.net/blog/best-practices" } + ]; + + resources.forEach(res => { + doc.setTextColor(...colors.primary); + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.textWithLink(`• ${res.title}`, margin, currentY, { url: `https://${res.url}` }); + currentY += 5; + }); + + currentY += 12; + + // --- Call to Action --- + // No page check needed, we are on Page 2 + + doc.setTextColor(...colors.text); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Next Steps', margin, currentY); + + currentY += 8; + doc.setTextColor(...colors.textLight); + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text('Ready to eliminate reprint costs? Create your account today.', margin, currentY); + + currentY += 8; // FIXED: Restored spacing to prevent overlap + doc.setTextColor(...colors.primary); + doc.setFont('helvetica', 'bold'); + doc.textWithLink('www.qrmaster.net/signup', margin, currentY, { url: 'https://www.qrmaster.net/signup' }); + + currentY += 20; + + // --- Why Dynamic? (Value Add) --- + // If we are close to bottom, force page 2 for this section to make it look intentional + if (currentY > pageHeight - 60) { + doc.addPage(); + currentY = 40; + } + + doc.setTextColor(...colors.text); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Why Professionals Choose Dynamic', margin, currentY); + currentY += 10; + + const benefits = [ + { title: "Real-time Control", desc: "Update destinations instantly. Fix mistakes locally without reprinting." }, + { title: "Smart Analytics", desc: "Track scans, locations, and device types to measure ROI." }, + { title: "Brand Identity", desc: "Fully customizable designs that match your corporate identity." } + ]; + + benefits.forEach(benefit => { + // Bullet + doc.setFillColor(...colors.success); // Green bullets + doc.circle(margin + 1, currentY - 1, 1.5, 'F'); + + // Title + doc.setTextColor(...colors.text); + doc.setFontSize(10); + doc.setFont('helvetica', 'bold'); + doc.text(benefit.title, margin + 6, currentY); + + // Desc + const titleWidth = doc.getStringUnitWidth(benefit.title) * 10 / doc.internal.scaleFactor; + doc.setTextColor(...colors.textLight); + doc.setFont('helvetica', 'normal'); + doc.text(`- ${benefit.desc}`, margin + 6 + titleWidth + 2, currentY); + + currentY += 7; + }); + + // --- Footer --- + const footerY = pageHeight - 15; + const pageCount = doc.getNumberOfPages(); + + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setDrawColor(...colors.border); + doc.line(margin, footerY - 10, pageWidth - margin, footerY - 10); + + doc.setTextColor(...colors.textLight); + doc.setFontSize(8); + doc.setFont('helvetica', 'normal'); + doc.text('© 2026 QR Master. All rights reserved.', margin, footerY); + doc.text('www.qrmaster.net', pageWidth - margin, footerY, { align: 'right' }); + } + + // Save + doc.save('QRMaster_Savings_Analysis.pdf'); } diff --git a/src/middleware.ts b/src/middleware.ts index ddd0a4c..3f38e3b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -19,6 +19,7 @@ export function middleware(req: NextRequest) { '/dynamic-qr-code-generator', '/bulk-qr-code-generator', '/qr-code-tracking', + '/reprint-calculator', ]; // Check if path is public