Lead Magnet

This commit is contained in:
Timo Knuth 2026-01-16 12:44:13 +01:00
parent 83ea141230
commit be54d388bb
20 changed files with 1211 additions and 21 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

205
package-lock.json generated
View File

@ -25,6 +25,7 @@
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"i18next": "^23.7.6", "i18next": "^23.7.6",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jspdf": "^4.0.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^14.2.35", "next": "^14.2.35",
@ -2105,6 +2106,12 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/papaparse": { "node_modules/@types/papaparse": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.0.tgz", "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.0.tgz",
@ -2132,6 +2139,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.26", "version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
@ -2153,6 +2167,13 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.51.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
@ -3153,6 +3174,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -3470,6 +3501,26 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chainsaw": { "node_modules/chainsaw": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
@ -3759,6 +3810,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4166,6 +4227,16 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -5038,6 +5109,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-png/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/fast-sha256": { "node_modules/fast-sha256": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
@ -5691,6 +5779,20 @@
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/i18next": { "node_modules/i18next": {
"version": "23.16.8", "version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
@ -5818,6 +5920,12 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/ioredis": { "node_modules/ioredis": {
"version": "5.8.2", "version": "5.8.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
@ -6408,6 +6516,29 @@
"json5": "lib/cli.js" "json5": "lib/cli.js"
} }
}, },
"node_modules/jspdf": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz",
"integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf/node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@ -7371,6 +7502,13 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -7802,6 +7940,16 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -8011,6 +8159,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -8125,6 +8280,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -8527,6 +8692,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/standard-as-callback": { "node_modules/standard-as-callback": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
@ -8888,6 +9063,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/svix": { "node_modules/svix": {
"version": "1.76.1", "version": "1.76.1",
"resolved": "https://registry.npmjs.org/svix/-/svix-1.76.1.tgz", "resolved": "https://registry.npmjs.org/svix/-/svix-1.76.1.tgz",
@ -9012,6 +9197,16 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -9459,6 +9654,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "13.0.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",

View File

@ -42,6 +42,7 @@
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"i18next": "^23.7.6", "i18next": "^23.7.6",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jspdf": "^4.0.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^14.2.35", "next": "^14.2.35",

View File

@ -162,3 +162,17 @@ model NewsletterSubscription {
@@index([email]) @@index([email])
@@index([createdAt]) @@index([createdAt])
} }
model Lead {
id String @id @default(cuid())
email String
source String @default("reprint-calculator")
reprintCost Float?
updatesPerYear Int?
annualSavings Float?
createdAt DateTime @default(now())
@@index([email])
@@index([createdAt])
@@index([source])
}

View File

@ -25,6 +25,7 @@ async function submitIndexNow() {
'/qr-code-tracking', '/qr-code-tracking',
'/dynamic-qr-code-generator', '/dynamic-qr-code-generator',
'/bulk-qr-code-generator', '/bulk-qr-code-generator',
'/reprint-calculator',
'/newsletter', '/newsletter',
]; ];

View File

@ -641,26 +641,35 @@ Product C,https://example.com/product-c,Budget Widget,electronics,sale`}
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="py-20 bg-gradient-to-r from-green-600 to-blue-600 text-white"> {/* CTA Section */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> <section className="py-24 bg-slate-900 relative overflow-hidden">
<h2 className="text-4xl font-bold mb-6"> {/* Background Decorations */}
Generate 1000s of QR Codes in Minutes <div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl opacity-50" />
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-green-500/20 rounded-full blur-3xl opacity-50" />
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center relative z-10">
<h2 className="text-4xl lg:text-5xl font-bold mb-6 text-white tracking-tight">
Ready to Generate <span className="text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-blue-400">1000s of Codes?</span>
</h2> </h2>
<p className="text-xl mb-8 text-green-100"> <p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
Save hours of manual work. Upload your file and get all QR codes ready instantly. Stop doing it manually. Upload your Excel file and get your QR codes in seconds. Professional, branded, and trackable.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-5 justify-center">
<Link href="/signup"> <Link href="/signup">
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-green-600 hover:bg-gray-100"> <Button size="lg" className="text-lg px-8 py-6 h-auto w-full sm:w-auto bg-white text-slate-900 hover:bg-slate-50 font-bold shadow-xl shadow-blue-900/20 transition-all hover:-translate-y-1">
Start Bulk Generation Start Bulk Generation
</Button> </Button>
</Link> </Link>
<Link href="/pricing"> <Link href="/pricing">
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10"> <Button size="lg" variant="outline" className="text-lg px-8 py-6 h-auto w-full sm:w-auto border-slate-700 text-white hover:bg-slate-800 hover:border-slate-600 transition-all">
View Pricing View Pricing
</Button> </Button>
</Link> </Link>
</div> </div>
<p className="mt-8 text-sm text-slate-500">
No credit card required for free trial.
</p>
</div> </div>
</section> </section>
</div> </div>

View File

@ -20,6 +20,8 @@ import {
Zap, Zap,
Send, Send,
CheckCircle2, CheckCircle2,
FileDown,
DollarSign,
} from 'lucide-react'; } from 'lucide-react';
interface AdminStats { interface AdminStats {
@ -81,6 +83,20 @@ export default function NewsletterClient() {
message: string; message: string;
} | null>(null); } | null>(null);
// Lead management state
const [leadData, setLeadData] = useState<{
total: number;
recent: Array<{
id: string;
email: string;
source: string;
reprintCost: number | null;
updatesPerYear: number | null;
annualSavings: number | null;
createdAt: string;
}>;
} | null>(null);
useEffect(() => { useEffect(() => {
checkAuth(); checkAuth();
}, []); }, []);
@ -93,8 +109,9 @@ export default function NewsletterClient() {
const data = await response.json(); const data = await response.json();
setStats(data); setStats(data);
setLoading(false); setLoading(false);
// Also fetch newsletter data // Also fetch newsletter and lead data
fetchNewsletterData(); fetchNewsletterData();
fetchLeadsData();
} else { } else {
setIsAuthenticated(false); setIsAuthenticated(false);
} }
@ -117,6 +134,18 @@ export default function NewsletterClient() {
} }
}; };
const fetchLeadsData = async () => {
try {
const response = await fetch('/api/leads');
if (response.ok) {
const data = await response.json();
setLeadData(data);
}
} catch (error) {
console.error('Failed to fetch leads data:', error);
}
};
const handleSendBroadcast = async () => { const handleSendBroadcast = async () => {
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) { if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
return; return;
@ -641,6 +670,84 @@ export default function NewsletterClient() {
</div> </div>
</Card> </Card>
</div> </div>
{/* Lead Management Section */}
<div className="mt-8">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-100 to-teal-100 dark:from-emerald-900/30 dark:to-teal-900/30 rounded-lg flex items-center justify-center">
<FileDown className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Lead Management</h3>
<p className="text-xs text-muted-foreground">Reprint Calculator PDF downloads</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{leadData?.total || 0}</span>
<p className="text-xs text-muted-foreground">Total Leads</p>
</div>
<Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
Active
</Badge>
</div>
{/* Recent Leads */}
<div>
<h4 className="font-medium mb-3">Recent Leads</h4>
{leadData?.recent && leadData.recent.length > 0 ? (
<div className="space-y-2">
{leadData.recent.map((lead) => (
<div
key={lead.id}
className="flex items-center justify-between py-3 px-4 border border-border rounded-lg bg-gray-50/50 dark:bg-gray-900/30"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<Mail className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0">
<span className="text-sm font-medium block truncate">{lead.email}</span>
{lead.annualSavings && (
<span className="text-xs text-emerald-600 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
{lead.annualSavings.toLocaleString()} potential savings
</span>
)}
</div>
</div>
<div className="text-right flex-shrink-0 ml-4">
<span className="text-xs text-muted-foreground block">
{new Date(lead.createdAt).toLocaleDateString()}
</span>
{lead.reprintCost && lead.updatesPerYear && (
<span className="text-xs text-slate-500">
{lead.reprintCost} × {lead.updatesPerYear}/yr
</span>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No leads yet. Leads appear when users download a PDF report from the Reprint Calculator.</p>
)}
</div>
{/* Tip */}
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-muted-foreground">
💡 Tip: View all leads in{' '}
<a
href="http://localhost:5555"
target="_blank"
rel="noopener noreferrer"
className="text-emerald-600 dark:text-emerald-400 hover:underline"
>
Prisma Studio
</a>
{' '}(Lead table)
</p>
</div>
</Card>
</div>
</div> </div>
</div> </div>
); );

View File

@ -378,26 +378,35 @@ export default function QRCodeTrackingPage() {
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white"> {/* CTA Section */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> <section className="py-24 bg-slate-900 relative overflow-hidden">
<h2 className="text-4xl font-bold mb-6"> {/* Background Decorations */}
Start Tracking Your QR Codes Today <div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl opacity-50" />
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-purple-500/20 rounded-full blur-3xl opacity-50" />
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center relative z-10">
<h2 className="text-4xl lg:text-5xl font-bold mb-6 text-white tracking-tight">
Start Tracking Your <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-purple-400">QR Codes Today</span>
</h2> </h2>
<p className="text-xl mb-8 text-primary-100"> <p className="text-xl mb-10 text-slate-300 leading-relaxed max-w-2xl mx-auto">
Join thousands of businesses using QR Master to track and optimize their QR code campaigns Join thousands of businesses using QR Master to optimize their campaigns with real-time analytics.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-5 justify-center">
<Link href="/signup"> <Link href="/signup">
<Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-primary-600 hover:bg-gray-100"> <Button size="lg" className="text-lg px-8 py-6 h-auto w-full sm:w-auto bg-white text-slate-900 hover:bg-slate-50 font-bold shadow-xl shadow-primary-900/20 transition-all hover:-translate-y-1">
Create Free Account Create Free Account
</Button> </Button>
</Link> </Link>
<Link href="/pricing"> <Link href="/pricing">
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10"> <Button size="lg" variant="outline" className="text-lg px-8 py-6 h-auto w-full sm:w-auto border-slate-700 text-white hover:bg-slate-800 hover:border-slate-600 transition-all">
View Pricing View Pricing
</Button> </Button>
</Link> </Link>
</div> </div>
<p className="mt-8 text-sm text-slate-500">
Full analytics accessible on free plan.
</p>
</div> </div>
</section> </section>
</div> </div>

View File

@ -0,0 +1,117 @@
import React from 'react';
import type { Metadata } from 'next';
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
export const metadata: Metadata = {
title: 'Reprint Cost Calculator | QR Master',
description:
'Calculate how much you are wasting on QR code reprints. See your potential savings with dynamic QR codes that never need to be reprinted.',
alternates: {
canonical: 'https://www.qrmaster.net/reprint-calculator',
},
robots: {
index: true,
follow: true,
},
openGraph: {
title: 'Reprint Cost Calculator | QR Master',
description: 'Stop wasting money on reprints. Calculate your savings now.',
url: 'https://www.qrmaster.net/reprint-calculator',
type: 'website',
images: [
{
url: 'https://www.qrmaster.net/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master Reprint Cost Calculator',
},
],
},
};
export default function ReprintCalculatorPage() {
return (
<>
{/* Hero Section */}
<section className="pt-24 pb-12 bg-white relative overflow-hidden">
<div className="container mx-auto px-4 text-center max-w-3xl relative z-10">
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-slate-100/80 backdrop-blur-sm border border-slate-200 text-slate-600 text-sm font-medium mb-8">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span>
Static QR codes are costing you money
</div>
<h1 className="text-4xl lg:text-6xl font-black text-slate-900 mb-6 tracking-tight leading-[1.1]">
Stop Burning Budget on <br className="hidden md:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-orange-600">Avoidable Reprints</span>
</h1>
<p className="text-xl text-slate-600 mb-8 leading-relaxed max-w-2xl mx-auto">
Every time a URL changes, static QR codes become useless trash.
Dynamic QR codes update instantlykeeping your print materials alive forever.
</p>
<div className="flex justify-center">
<ArrowDown className="w-6 h-6 text-slate-400 animate-bounce" />
</div>
</div>
</section>
{/* Calculator Component */}
<ReprintSavingsCalculator />
{/* Value Props */}
<section className="py-24 bg-white border-t border-slate-100">
<div className="container mx-auto px-4 max-w-6xl">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Why Smart Companies Switched Years Ago
</h2>
<p className="text-slate-600 text-lg max-w-2xl mx-auto">
The math is simple. One dynamic subscription costs less than a single batch of reprints.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 lg:gap-12">
{[
{
icon: Zap,
color: "text-amber-500",
bg: "bg-amber-50",
title: "Update Instantly",
desc: "Changed your menu? New promo link? Update the destination in seconds. Your printed codes keep working perfectly."
},
{
icon: ShieldCheck,
color: "text-blue-500",
bg: "bg-blue-50",
title: "Error Proofing",
desc: "Printed the wrong link? With static codes, that's a disaster. With dynamic codes, it's a 5-second fix in the dashboard."
},
{
icon: Check,
color: "text-green-500",
bg: "bg-green-50",
title: "Real ROI Tracking",
desc: "Stop guessing if your print ads work. Track every scan, location, and device to measure exactly what's driving value."
}
].map((feature, i) => (
<div key={i} className="group p-8 rounded-2xl bg-slate-50 border border-slate-100 hover:bg-white hover:shadow-xl hover:shadow-slate-200/50 hover:border-slate-200 transition-all duration-300">
<div className={`w-14 h-14 ${feature.bg} rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}>
<feature.icon className={`w-7 h-7 ${feature.color}`} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-3">{feature.title}</h3>
<p className="text-slate-600 leading-relaxed">
{feature.desc}
</p>
</div>
))}
</div>
</div>
</section>
</>
);
}

View File

@ -0,0 +1,81 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
interface LeadInput {
email: string;
source?: string;
reprintCost?: number;
updatesPerYear?: number;
annualSavings?: number;
}
export async function POST(request: Request) {
try {
const body: LeadInput = await request.json();
const { email, source, reprintCost, updatesPerYear, annualSavings } = body;
if (!email || !email.includes('@')) {
return NextResponse.json(
{ error: 'Valid email is required' },
{ status: 400 }
);
}
const lead = await (db as any).lead.create({
data: {
email: email.toLowerCase().trim(),
source: source || 'reprint-calculator',
reprintCost: reprintCost ? Number(reprintCost) : null,
updatesPerYear: updatesPerYear ? Number(updatesPerYear) : null,
annualSavings: annualSavings ? Number(annualSavings) : null,
},
});
return NextResponse.json({ success: true, id: lead.id });
} catch (error) {
console.error('Error saving lead:', error);
return NextResponse.json(
{ error: 'Failed to save lead' },
{ status: 500 }
);
}
}
export async function GET() {
try {
const [leads, total] = await Promise.all([
(db as any).lead.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
}),
(db as any).lead.count(),
]);
return NextResponse.json({
total,
recent: leads.map((lead: {
id: string;
email: string;
source: string;
reprintCost: number | null;
updatesPerYear: number | null;
annualSavings: number | null;
createdAt: Date;
}) => ({
id: lead.id,
email: lead.email,
source: lead.source,
reprintCost: lead.reprintCost,
updatesPerYear: lead.updatesPerYear,
annualSavings: lead.annualSavings,
createdAt: lead.createdAt.toISOString(),
})),
});
} catch (error) {
console.error('Error fetching leads:', error);
return NextResponse.json(
{ error: 'Failed to fetch leads' },
{ status: 500 }
);
}
}

View File

@ -61,6 +61,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly', changeFrequency: 'monthly',
priority: 0.9, priority: 0.9,
}, },
{
url: `${baseUrl}/reprint-calculator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{ {
url: `${baseUrl}/dynamic-qr-code-generator`, url: `${baseUrl}/dynamic-qr-code-generator`,
lastModified: new Date(), lastModified: new Date(),

View File

@ -11,6 +11,7 @@ import { Features } from '@/components/marketing/Features';
import { Pricing } from '@/components/marketing/Pricing'; import { Pricing } from '@/components/marketing/Pricing';
import { FAQ } from '@/components/marketing/FAQ'; import { FAQ } from '@/components/marketing/FAQ';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { ReprintCalculatorTeaser } from '@/components/marketing/ReprintCalculatorTeaser';
import { ScrollToTop } from '@/components/ui/ScrollToTop'; import { ScrollToTop } from '@/components/ui/ScrollToTop';
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid'; import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
import en from '@/i18n/en.json'; import en from '@/i18n/en.json';
@ -40,8 +41,11 @@ export default function HomePageClient() {
{/* Free Tools Grid */} {/* Free Tools Grid */}
<FreeToolsGrid /> <FreeToolsGrid />
<StaticVsDynamic t={t} /> <React.Fragment>
<Features t={t} /> <StaticVsDynamic t={t} />
<ReprintCalculatorTeaser />
<Features t={t} />
</React.Fragment>
{/* Pricing Section */} {/* Pricing Section */}
<Pricing t={t} /> <Pricing t={t} />

View File

@ -0,0 +1,62 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { ArrowRight, Calculator, TrendingUp } from 'lucide-react';
export const ReprintCalculatorTeaser: React.FC = () => {
return (
<section className="py-24 bg-white relative overflow-hidden">
<div className="container mx-auto px-4 relative z-10">
<div className="bg-gradient-to-br from-indigo-50 to-white border border-indigo-100 rounded-3xl p-8 md:p-12 lg:p-16 flex flex-col md:flex-row items-center justify-between gap-12 shadow-sm hover:shadow-md transition-shadow duration-500">
<div className="flex-1 text-center md:text-left">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-sm font-medium mb-6">
<TrendingUp className="w-4 h-4" />
<span>ROI Calculator</span>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-6 leading-tight">
Are you burning budget on <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-blue-600">Static Reprints?</span>
</h2>
<p className="text-slate-600 text-lg md:text-xl max-w-xl mx-auto md:mx-0 leading-relaxed mb-8">
Find out exactly how much you can save by switching to dynamic QR codes. Our calculator reveals your savings potential in seconds.
</p>
<Link href="/reprint-calculator">
<Button
size="lg"
variant="primary"
className="h-14 px-8 text-lg hover:translate-x-1 transition-transform"
>
Calculate Savings <ArrowRight className="w-5 h-5 ml-2" />
</Button>
</Link>
</div>
<div className="flex-shrink-0 w-full md:w-auto flex justify-center md:justify-end">
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-indigo-500 to-blue-500 rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200"></div>
<div className="relative bg-white rounded-xl p-8 border border-slate-100 w-full max-w-xs text-center shadow-lg">
<div className="w-16 h-16 bg-indigo-50 rounded-2xl flex items-center justify-center mx-auto mb-6 text-indigo-600">
<Calculator className="w-8 h-8" />
</div>
<h3 className="text-lg font-bold text-slate-900 mb-2">Cost Analysis</h3>
<p className="text-slate-500 text-sm mb-6">
Enter your print volume and update frequency to see your hidden costs.
</p>
<div className="h-1.5 w-full bg-slate-100 rounded-full overflow-hidden">
<div className="h-full w-2/3 bg-indigo-500 rounded-full animate-pulse"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,430 @@
'use client';
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { AlertTriangle, CheckCircle, TrendingDown, ArrowRight, RefreshCcw, FileDown, X, Loader2, Mail } from 'lucide-react';
import { generateSavingsReport } from '@/lib/generateSavingsReport';
interface CalculatorResults {
annualWaste: number;
withQRMaster: number;
savings: number;
savingsPercent: number;
monthsPaidFor: number;
}
export const ReprintSavingsCalculator: React.FC = () => {
const [reprintCost, setReprintCost] = useState<string>('');
const [updatesPerYear, setUpdatesPerYear] = useState<string>('');
const [showResults, setShowResults] = useState(false);
const [showEmailModal, setShowEmailModal] = useState(false);
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState('');
// QR Master Pro yearly cost
const qrMasterYearlyCost = 108; // €9/month * 12
const results = useMemo((): CalculatorResults | null => {
const cost = parseFloat(reprintCost);
const updates = parseFloat(updatesPerYear);
if (!cost || !updates || cost <= 0 || updates <= 0) {
return null;
}
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));
return {
annualWaste,
withQRMaster,
savings: Math.max(0, savings),
savingsPercent: Math.max(0, savingsPercent),
monthsPaidFor,
};
}, [reprintCost, updatesPerYear]);
const handleCalculate = () => {
if (results) {
setShowResults(true);
}
};
const handleReset = () => {
setShowResults(false);
setReprintCost('');
setUpdatesPerYear('');
};
const handleDownloadReport = async () => {
setShowEmailModal(true);
};
const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !email.includes('@') || !results) return;
setIsSubmitting(true);
setSubmitError('');
try {
// Save lead to database
const response = await fetch('/api/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
source: 'reprint-calculator',
reprintCost: parseFloat(reprintCost),
updatesPerYear: parseInt(updatesPerYear),
annualSavings: results.savings,
}),
});
if (!response.ok) {
throw new Error('Failed to save');
}
// Generate and download PDF
generateSavingsReport({
reprintCost: parseFloat(reprintCost),
updatesPerYear: parseInt(updatesPerYear),
annualWaste: results.annualWaste,
qrMasterCost: results.withQRMaster,
savings: results.savings,
savingsPercent: results.savingsPercent,
});
// Close modal and reset
setShowEmailModal(false);
setEmail('');
} catch (error) {
setSubmitError('Something went wrong. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
return (
<>
<section className="py-20 bg-gradient-to-br from-slate-50 to-white relative overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none opacity-40">
<div className="absolute -top-[20%] -right-[10%] w-[600px] h-[600px] bg-primary-100/50 rounded-full blur-3xl" />
<div className="absolute bottom-[0%] -left-[10%] w-[500px] h-[500px] bg-indigo-50/50 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden"
>
<div className="grid md:grid-cols-5 h-full">
{/* Left Side: Form */}
<div className={`p-8 md:p-10 ${showResults ? 'md:col-span-2 bg-slate-50 border-r border-slate-100' : 'md:col-span-5'}`}>
<AnimatePresence mode="wait">
{!showResults ? (
<motion.div
key="form-intro"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="max-w-2xl mx-auto md:max-w-none text-center md:text-left"
>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-red-50 text-red-600 text-sm font-medium mb-6">
<AlertTriangle className="w-4 h-4" />
Stop wasting budget
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-4 leading-tight">
Calculate your Reprint Waste
</h2>
<p className="text-slate-600 mb-8">
Enter your printing costs to see exactly how much static QR codes are costing you annually.
</p>
</motion.div>
) : (
<motion.div
key="form-minimized"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<h3 className="font-bold text-slate-900 mb-6 flex items-center gap-2">
<RefreshCcw className="w-4 h-4 text-slate-400" />
Parameters
</h3>
</motion.div>
)}
</AnimatePresence>
<div className="space-y-6">
<div>
<label htmlFor="reprintCost" className="block text-sm font-semibold text-slate-700 mb-2">
Reprint cost per batch
</label>
<div className="relative group">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-medium text-lg transition-colors group-hover:text-primary-600"></span>
<input
type="number"
id="reprintCost"
value={reprintCost}
onChange={(e) => setReprintCost(e.target.value)}
placeholder="500"
min="0"
className="w-full pl-10 pr-4 py-3.5 text-lg border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm group-hover:border-primary-300 bg-white"
/>
</div>
</div>
<div>
<label htmlFor="updatesPerYear" className="block text-sm font-semibold text-slate-700 mb-2">
URL updates per year
</label>
<input
type="number"
id="updatesPerYear"
value={updatesPerYear}
onChange={(e) => setUpdatesPerYear(e.target.value)}
placeholder="3"
min="0"
className="w-full px-4 py-3.5 text-lg border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm hover:border-primary-300 bg-white"
/>
</div>
{!showResults && (
<Button
onClick={handleCalculate}
disabled={!reprintCost || !updatesPerYear}
variant="primary"
size="lg"
className="w-full text-lg py-4 shadow-lg shadow-primary-500/20 hover:shadow-primary-500/40 transition-all transform hover:-translate-y-0.5"
>
Calculate Savings <ArrowRight className="ml-2 w-5 h-5" />
</Button>
)}
{showResults && (
<button
onClick={handleCalculate} // Recalculate if changed
className="text-sm text-primary-600 font-medium hover:text-primary-700 underline decoration-2 decoration-transparent hover:decoration-primary-200 transition-all"
>
Update Calculation
</button>
)}
</div>
</div>
{/* Right Side: Results */}
<div className={`bg-white p-8 md:p-10 flex flex-col justify-center transition-all duration-500 ${showResults ? 'md:col-span-3 opacity-100 translate-x-0' : 'hidden md:flex md:col-span-0 opacity-0 translate-x-10 w-0 p-0 overflow-hidden'}`}>
{results && showResults && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
className="h-full flex flex-col"
>
<div className="flex-1">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-green-50 text-green-700 text-sm font-medium mb-6">
<TrendingDown className="w-4 h-4" />
Potential Savings Found
</div>
<div className="mb-8">
<p className="text-slate-500 text-sm font-medium uppercase tracking-wide mb-1">Annual Waste Identified</p>
<div className="flex items-baseline gap-2">
<span className="text-5xl lg:text-6xl font-black text-slate-900 tracking-tight">
{formatCurrency(results.annualWaste)}
</span>
</div>
<p className="text-red-500 text-sm mt-2 font-medium flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> Money currently lost to static reprints
</p>
</div>
{/* Comparison Bars */}
<div className="space-y-6 mb-10">
<div className="relative">
<div className="flex justify-between text-sm mb-2">
<span className="font-semibold text-slate-700">Static QR Code Cost</span>
<span className="font-bold text-red-600">{formatCurrency(results.annualWaste)}</span>
</div>
<div className="h-3 bg-slate-100 rounded-full w-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: "100%" }}
transition={{ duration: 1, ease: "easeOut" }}
className="h-full bg-red-500 rounded-full"
/>
</div>
</div>
<div className="relative">
<div className="flex justify-between text-sm mb-2">
<span className="font-semibold text-slate-700">QR Master Cost</span>
<span className="font-bold text-emerald-600">{formatCurrency(results.withQRMaster)}</span>
</div>
<div className="h-3 bg-slate-100 rounded-full w-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(results.withQRMaster / results.annualWaste) * 100}%` }}
transition={{ duration: 1, ease: "easeOut", delay: 0.5 }}
className="h-full bg-emerald-500 rounded-full"
/>
</div>
</div>
</div>
<div className="bg-emerald-50/50 border border-emerald-100 rounded-2xl p-6 mb-8">
<div className="flex items-start gap-4">
<div className="p-3 bg-emerald-100 rounded-xl text-emerald-600 shrink-0">
<CheckCircle className="w-6 h-6" />
</div>
<div>
<h4 className="text-lg font-bold text-emerald-900 mb-1">
You save {results.savingsPercent}% instantly
</h4>
<p className="text-emerald-800/80 leading-relaxed">
That's <strong className="text-emerald-900">{formatCurrency(results.savings)}</strong> extra budget per year.
Effectively getting <strong className="text-emerald-900">{results.monthsPaidFor} months</strong> of service for free compared to just one single reprint.
</p>
</div>
</div>
</div>
</div>
<div className="mt-auto space-y-3">
<Link href="/signup" className="block">
<Button variant="primary" size="lg" className="w-full text-lg py-4 shadow-xl shadow-primary-500/20 hover:shadow-primary-500/30">
Start Saving Now
</Button>
</Link>
<button
onClick={handleDownloadReport}
className="w-full flex items-center justify-center gap-2 py-3 px-4 text-sm font-medium text-slate-700 bg-slate-100 hover:bg-slate-200 rounded-xl transition-colors"
>
<FileDown className="w-4 h-4" />
Download Your Savings Report (PDF)
</button>
<p className="text-center text-xs text-slate-400">
Based on QR Master Pro annual plan. No credit card required to start.
</p>
</div>
</motion.div>
)}
</div>
</div>
</motion.div>
</div>
</section>
{/* Email Capture Modal */}
<AnimatePresence>
{showEmailModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={() => setShowEmailModal(false)}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-8 relative"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setShowEmailModal(false)}
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-600 rounded-lg hover:bg-slate-100 transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="text-center mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-primary-100 to-indigo-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<FileDown className="w-8 h-8 text-primary-600" />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-2">
Get Your Savings Report
</h3>
<p className="text-slate-600">
Enter your email to receive your personalized PDF report with detailed cost analysis.
</p>
</div>
<form onSubmit={handleEmailSubmit} className="space-y-4">
<div>
<label htmlFor="emailInput" className="block text-sm font-medium text-slate-700 mb-2">
Business Email
</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="email"
id="emailInput"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@company.com"
required
className="w-full pl-12 pr-4 py-3.5 text-lg border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all"
/>
</div>
</div>
{submitError && (
<p className="text-sm text-red-600">{submitError}</p>
)}
<Button
type="submit"
disabled={isSubmitting || !email}
variant="primary"
size="lg"
className="w-full text-lg py-4"
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<FileDown className="w-5 h-5 mr-2" />
Download Report
</>
)}
</Button>
<p className="text-xs text-slate-400 text-center">
We respect your privacy. Your email will only be used for QR Master updates.
</p>
</form>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default ReprintSavingsCalculator;

View File

@ -45,6 +45,8 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<li><Link href="/bulk-qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bulk QR Generator</Link></li> <li><Link href="/bulk-qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bulk QR Generator</Link></li>
<li><Link href="/signup" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.get_started}</Link></li> <li><Link href="/signup" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.get_started}</Link></li>
<li><Link href="/reprint-calculator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Reprint Cost Calculator</Link></li>
<li><Link href="/qr-code-tracking" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Our Analytics</Link></li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,142 @@
import { jsPDF } from 'jspdf';
interface ReportData {
reprintCost: number;
updatesPerYear: number;
annualWaste: number;
qrMasterCost: number;
savings: number;
savingsPercent: number;
}
export function generateSavingsReport(data: ReportData): void {
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.getWidth();
// 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
// Header section with brand color
doc.setFillColor(...primaryColor);
doc.rect(0, 0, pageWidth, 45, 'F');
// 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);
doc.setFontSize(24);
doc.setFont('helvetica', 'bold');
doc.text(`${data.savings.toLocaleString()}`, 28, 262);
doc.setFontSize(14);
doc.text(`(${data.savingsPercent}%)`, 90, 262);
// 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);
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);
// Website link
doc.setTextColor(...primaryColor);
doc.setFont('helvetica', 'bold');
doc.text('Start your free trial: www.qrmaster.net/signup', 20, 332);
// Footer
doc.setTextColor(...grayColor);
doc.setFontSize(8);
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' });
// Download the PDF
doc.save('QRMaster-Savings-Report.pdf');
}