diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e88e3d8..742ef70 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,9 @@ "Bash(git remote add:*)", "Bash(git push:*)", "Bash(git remote set-url:*)", - "Bash(npm install:*)" + "Bash(npm install:*)", + "Bash(npm run build:*)", + "Bash(ls:*)" ], "deny": [], "ask": [] diff --git a/LICENSE b/LICENSE index bf90846..f4a8a51 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 QR Master +Copyright (c) 2025 QR Master Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package-lock.json b/package-lock.json index bd76b3a..b3d4f3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,17 +10,21 @@ "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^1.0.12", + "@edge-runtime/cookies": "^6.0.0", "@prisma/client": "^5.7.0", "@stripe/stripe-js": "^8.0.0", "bcryptjs": "^2.4.3", "chart.js": "^4.4.0", "clsx": "^2.0.0", "dayjs": "^1.11.10", + "file-saver": "^2.0.5", "i18next": "^23.7.6", "ioredis": "^5.3.2", + "jszip": "^3.10.1", "next": "14.2.18", "next-auth": "^4.24.5", "papaparse": "^5.4.1", + "posthog-js": "^1.276.0", "qrcode": "^1.5.3", "qrcode.react": "^3.1.0", "react": "^18.2.0", @@ -31,11 +35,13 @@ "sharp": "^0.33.1", "stripe": "^19.1.0", "tailwind-merge": "^2.2.0", + "uuid": "^13.0.0", "xlsx": "^0.18.5", "zod": "^3.22.4" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", + "@types/file-saver": "^2.0.7", "@types/node": "^20.10.5", "@types/papaparse": "^5.3.14", "@types/qrcode": "^1.5.5", @@ -128,6 +134,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@edge-runtime/cookies": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/cookies/-/cookies-6.0.0.tgz", + "integrity": "sha512-VVO/8AwC2qVbygLb2IOkX1zWFx2yWIHzFv4D602CTnoRffd/+cdcXqpSydKaedFrk7a1dRYXbWwjzfV/gwZ2Gw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -1521,6 +1536,12 @@ "node": ">=14" } }, + "node_modules/@posthog/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz", + "integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==", + "license": "MIT" + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", @@ -1652,6 +1673,13 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -3370,6 +3398,23 @@ "node": ">= 0.6" } }, + "node_modules/core-js": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -4418,6 +4463,12 @@ } } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4431,6 +4482,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/file-selector": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", @@ -4948,6 +5005,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4991,7 +5054,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -5609,6 +5671,18 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5653,6 +5727,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -5957,6 +6040,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/next-auth/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/next-sitemap": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz", @@ -6319,6 +6411,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", @@ -6629,6 +6727,41 @@ "dev": true, "license": "MIT" }, + "node_modules/posthog-js": { + "version": "1.276.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.276.0.tgz", + "integrity": "sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@posthog/core": "1.3.0", + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@rrweb/types": "2.0.0-alpha.17", + "rrweb-snapshot": "2.0.0-alpha.17" + }, + "peerDependenciesMeta": { + "@rrweb/types": { + "optional": true + }, + "rrweb-snapshot": { + "optional": true + } + } + }, + "node_modules/posthog-js/node_modules/preact": { + "version": "10.27.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/preact": { "version": "10.11.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", @@ -6703,6 +6836,12 @@ "fsevents": "2.3.3" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6876,6 +7015,27 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7082,6 +7242,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -7193,6 +7359,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -7415,6 +7587,15 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8173,16 +8354,19 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/void-elements": { @@ -8194,6 +8378,12 @@ "node": ">=0.10.0" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index f43d7d0..feb5e4f 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,21 @@ }, "dependencies": { "@auth/prisma-adapter": "^1.0.12", + "@edge-runtime/cookies": "^6.0.0", "@prisma/client": "^5.7.0", "@stripe/stripe-js": "^8.0.0", "bcryptjs": "^2.4.3", "chart.js": "^4.4.0", "clsx": "^2.0.0", "dayjs": "^1.11.10", + "file-saver": "^2.0.5", "i18next": "^23.7.6", "ioredis": "^5.3.2", + "jszip": "^3.10.1", "next": "14.2.18", "next-auth": "^4.24.5", "papaparse": "^5.4.1", + "posthog-js": "^1.276.0", "qrcode": "^1.5.3", "qrcode.react": "^3.1.0", "react": "^18.2.0", @@ -47,11 +51,13 @@ "sharp": "^0.33.1", "stripe": "^19.1.0", "tailwind-merge": "^2.2.0", + "uuid": "^13.0.0", "xlsx": "^0.18.5", "zod": "^3.22.4" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", + "@types/file-saver": "^2.0.7", "@types/node": "^20.10.5", "@types/papaparse": "^5.3.14", "@types/qrcode": "^1.5.5", diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..afec598 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logo.svg b/public/logo.svg index 57a62fb..d351098 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,21 +1,32 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/(app)/analytics/page.tsx b/src/app/(app)/analytics/page.tsx index 93f37ae..f8a4e23 100644 --- a/src/app/(app)/analytics/page.tsx +++ b/src/app/(app)/analytics/page.tsx @@ -124,22 +124,23 @@ export default function AnalyticsPage() { URL.revokeObjectURL(url); }; - // Prepare chart data - const last7Days = Array.from({ length: 7 }, (_, i) => { + // Prepare chart data based on selected time range + const daysToShow = parseInt(timeRange); + const dateRange = Array.from({ length: daysToShow }, (_, i) => { const date = new Date(); - date.setDate(date.getDate() - (6 - i)); + date.setDate(date.getDate() - (daysToShow - 1 - i)); return date.toISOString().split('T')[0]; }); const scanChartData = { - labels: last7Days.map(date => { + labels: dateRange.map(date => { const d = new Date(date); return d.toLocaleDateString('en', { month: 'short', day: 'numeric' }); }), datasets: [ { label: 'Scans', - data: last7Days.map(date => analyticsData?.dailyScans[date] || 0), + data: dateRange.map(date => analyticsData?.dailyScans[date] || 0), borderColor: 'rgb(37, 99, 235)', backgroundColor: 'rgba(37, 99, 235, 0.1)', tension: 0.4, diff --git a/src/app/(app)/bulk-creation/page.tsx b/src/app/(app)/bulk-creation/page.tsx new file mode 100644 index 0000000..68ce852 --- /dev/null +++ b/src/app/(app)/bulk-creation/page.tsx @@ -0,0 +1,594 @@ +'use client'; + +import React, { useState, useCallback } from 'react'; +import { useDropzone } from 'react-dropzone'; +import Papa from 'papaparse'; +import * as XLSX from 'xlsx'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; +import { Select } from '@/components/ui/Select'; +import { QRCodeSVG } from 'qrcode.react'; +import { showToast } from '@/components/ui/Toast'; +import { useTranslation } from '@/hooks/useTranslation'; +import JSZip from 'jszip'; +import { saveAs } from 'file-saver'; + +interface BulkQRData { + title: string; + content: string; +} + +interface GeneratedQR { + title: string; + content: string; // Original URL + svg: string; // SVG markup +} + +export default function BulkCreationPage() { + const { t } = useTranslation(); + const [step, setStep] = useState<'upload' | 'preview' | 'complete'>('upload'); + const [data, setData] = useState([]); + const [mapping, setMapping] = useState>({}); + const [loading, setLoading] = useState(false); + const [generatedQRs, setGeneratedQRs] = useState([]); + const [userPlan, setUserPlan] = useState('FREE'); + + // Check user plan on mount + React.useEffect(() => { + const checkPlan = async () => { + try { + const response = await fetch('/api/user/plan'); + if (response.ok) { + const data = await response.json(); + setUserPlan(data.plan || 'FREE'); + } + } catch (error) { + console.error('Error checking plan:', error); + } + }; + checkPlan(); + }, []); + + const onDrop = useCallback((acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + if (!file) return; + + const reader = new FileReader(); + + if (file.name.endsWith('.csv')) { + reader.onload = (e) => { + const text = e.target?.result as string; + const result = Papa.parse(text, { header: true }); + processData(result.data); + }; + reader.readAsText(file); + } else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { + reader.onload = (e) => { + const data = new Uint8Array(e.target?.result as ArrayBuffer); + const workbook = XLSX.read(data, { type: 'array' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + processData(jsonData); + }; + reader.readAsArrayBuffer(file); + } + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: { + 'text/csv': ['.csv'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + }, + maxFiles: 1, + }); + + const processData = (rawData: any[]) => { + // Limit to 1000 rows + const limitedData = rawData.slice(0, 1000); + + // Auto-detect columns + if (limitedData.length > 0) { + const columns = Object.keys(limitedData[0]); + const autoMapping: Record = {}; + + columns.forEach((col) => { + const lowerCol = col.toLowerCase(); + if (lowerCol.includes('title') || lowerCol.includes('name') || lowerCol === 'test') { + autoMapping.title = col; + } else if (lowerCol.includes('content') || lowerCol.includes('url') || lowerCol.includes('data') || lowerCol.includes('link')) { + autoMapping.content = col; + } + }); + + // If no title column found, use first column + if (!autoMapping.title && columns.length > 0) { + autoMapping.title = columns[0]; + } + // If no content column found, use second column + if (!autoMapping.content && columns.length > 1) { + autoMapping.content = columns[1]; + } + + setMapping(autoMapping); + } + + setData(limitedData); + setStep('preview'); + }; + + const generateStaticQRCodes = async () => { + setLoading(true); + + try { + const qrCodes: GeneratedQR[] = []; + + // Generate all QR codes client-side (Static QR Codes) + for (const row of data) { + const title = row[mapping.title as keyof typeof row] || 'Untitled'; + const content = row[mapping.content as keyof typeof row] || 'https://example.com'; + + // Create a temporary div to render QR code + const tempDiv = document.createElement('div'); + tempDiv.style.display = 'none'; + document.body.appendChild(tempDiv); + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', '300'); + svg.setAttribute('height', '300'); + tempDiv.appendChild(svg); + + // Use qrcode library to generate SVG + const QRCode = require('qrcode'); + const qrSvg = await QRCode.toString(content, { + type: 'svg', + width: 300, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF', + }, + }); + + qrCodes.push({ + title: String(title), + content: String(content), // Store the original URL + svg: qrSvg, + }); + + document.body.removeChild(tempDiv); + } + + setGeneratedQRs(qrCodes); + setStep('complete'); + showToast(`Successfully generated ${qrCodes.length} static QR codes!`, 'success'); + } catch (error) { + console.error('QR generation error:', error); + showToast('Failed to generate QR codes', 'error'); + } finally { + setLoading(false); + } + }; + + const downloadAllQRCodes = async () => { + const zip = new JSZip(); + + generatedQRs.forEach((qr, index) => { + const fileName = `${qr.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${index + 1}.svg`; + zip.file(fileName, qr.svg); + }); + + const blob = await zip.generateAsync({ type: 'blob' }); + saveAs(blob, 'qr-codes-bulk.zip'); + showToast('Download started!', 'success'); + }; + + const saveQRCodesToDatabase = async () => { + setLoading(true); + + try { + const qrCodesToSave = generatedQRs.map((qr) => ({ + title: qr.title, + isStatic: true, // This tells the API it's a static QR code + contentType: 'URL', + content: { url: qr.content }, // Content needs to be an object with url property + status: 'ACTIVE', + })); + + // Save each QR code to the database + const savePromises = qrCodesToSave.map((qr) => + fetch('/api/qrs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(qr), + }) + ); + + const results = await Promise.all(savePromises); + const failedCount = results.filter((r) => !r.ok).length; + + if (failedCount === 0) { + showToast(`Successfully saved ${qrCodesToSave.length} QR codes!`, 'success'); + // Redirect to dashboard after 1 second + setTimeout(() => { + window.location.href = '/dashboard'; + }, 1000); + } else { + showToast(`Saved ${qrCodesToSave.length - failedCount} QR codes, ${failedCount} failed`, 'warning'); + } + } catch (error) { + console.error('Error saving QR codes:', error); + showToast('Failed to save QR codes', 'error'); + } finally { + setLoading(false); + } + }; + + const downloadTemplate = () => { + const template = [ + { title: 'Product Page', content: 'https://example.com/product' }, + { title: 'Landing Page', content: 'https://example.com/landing' }, + { title: 'Contact Form', content: 'https://example.com/contact' }, + ]; + + const csv = Papa.unparse(template); + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'bulk-qr-template.csv'; + a.click(); + URL.revokeObjectURL(url); + }; + + // Show upgrade prompt if not Business plan + if (userPlan !== 'BUSINESS') { + return ( +
+ + +
+ + + +
+

Business Plan Required

+

+ Bulk QR code creation is exclusively available for Business plan subscribers. + Upgrade now to generate up to 1,000 static QR codes at once. +

+
+ + +
+
+
+
+ ); + } + + return ( +
+
+

{t('bulk.title')}

+

{t('bulk.subtitle')}

+
+ + {/* Template Warning Banner */} + + +
+ + + +
+

Please Follow the Template Format

+

+ Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL). +

+
+
+
+
+ + {/* Info Banner */} + + +
+ + + +
+

Static QR Codes Only

+

+ Bulk creation generates static QR codes that cannot be edited after creation. + These QR codes do not include tracking or analytics. Perfect for print materials and offline use. +

+
+
+
+
+ + {/* Progress Steps */} +
+
+
+
+ 1 +
+ Upload File +
+ +
+
+
+ +
+
+ 2 +
+ Preview & Map +
+ +
+
+
+ +
+
+ 3 +
+ Download +
+
+
+ + {/* Upload Step */} + {step === 'upload' && ( + + +
+ +
+ +
+ + + + +

+ {isDragActive ? 'Drop the file here' : 'Drag & drop your file here'} +

+

or click to browse

+

Supports CSV, XLS, XLSX (max 1,000 rows)

+
+ +
+ + +
+
+ + + +
+
+

Simple Format

+

Just title & URL

+
+
+
+
+ + + +
+
+ + + +
+
+

Static QR Codes

+

No tracking included

+
+
+
+
+ + + +
+
+ + + +
+
+

Instant Download

+

Get ZIP with all SVGs

+
+
+
+
+
+
+
+ )} + + {/* Preview Step */} + {step === 'preview' && ( + + +
+ Preview & Map Columns + {data.length} rows detected +
+
+ +
+
+ + setMapping({ ...mapping, content: e.target.value })} + options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))} + /> +
+
+ +
+ + + + + + + + + + {data.slice(0, 5).map((row: any, index) => ( + + + + + + ))} + +
PreviewTitleContent
+ + + {row[mapping.title] || 'Untitled'} + + {(row[mapping.content] || '').substring(0, 50)}... +
+
+ + {data.length > 5 && ( +

+ Showing 5 of {data.length} rows +

+ )} + +
+ + +
+
+
+ )} + + {/* Complete Step */} + {step === 'complete' && ( + + +
+ + + +
+

Generation Complete!

+

+ Successfully generated {generatedQRs.length} static QR codes +

+ +
+
+ {generatedQRs.slice(0, 8).map((qr, index) => ( +
+
+
+
+

{qr.title}

+
+ ))} +
+
+ + + +
+ + + +
+ + + )} +
+ ); +} diff --git a/src/app/(app)/create/page.tsx b/src/app/(app)/create/page.tsx index 2a95708..ca92566 100644 --- a/src/app/(app)/create/page.tsx +++ b/src/app/(app)/create/page.tsx @@ -53,7 +53,7 @@ export default function CreatePage() { }; fetchUserPlan(); }, []); - + const contrast = calculateContrast(foregroundColor, backgroundColor); const hasGoodContrast = contrast >= 4.5; @@ -123,7 +123,7 @@ export default function CreatePage() { light: backgroundColor, }, }); - + const blob = new Blob([svg], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -165,9 +165,9 @@ export default function CreatePage() { size, }, }; - + console.log('SENDING QR DATA:', qrData); - + const response = await fetch('/api/qrs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -287,7 +287,8 @@ export default function CreatePage() { return (
-

Create QR Code

+

{t('create.title')}

+

{t('create.subtitle')}

@@ -307,21 +308,21 @@ export default function CreatePage() { placeholder="My QR Code" required /> - + setTags(e.target.value)} - placeholder="marketing, campaign, 2024" + placeholder="marketing, campaign, 2025" /> @@ -427,7 +428,7 @@ export default function CreatePage() {
- +
-

- Copy this URL to your Zapier trigger -

-
- -
- -
- - -
-
-

Sample Payload

-
-{`{
+                  
+ +
+ + + +
+
+ +
+

Sample Payload

+
+                      {`{
   "event": "qr_scanned",
   "qr_id": "abc123",
   "title": "Product Page",
-  "timestamp": "2024-01-01T12:00:00Z",
+  "timestamp": "2025-01-01T12:00:00Z",
   "location": "United States",
   "device": "mobile"
 }`}
-                  
-
- - )} +
+
+ + )} - {selectedIntegration.id === 'airtable' && ( - <> - setApiKey(e.target.value)} - placeholder="key..." - /> - - - - - )} - - {selectedIntegration.id === 'google-sheets' && ( - <> -
- -
- - - )} + + )} -
- - -
+ {selectedIntegration.id === 'google-sheets' && ( + <> +
+ +
+ + + )} + +
+ + +
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index b211c53..2d8672d 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -30,7 +30,7 @@ export default function AppLayout({ const navigation = [ { - name: 'Dashboard', + name: t('nav.dashboard'), href: '/dashboard', icon: ( @@ -39,7 +39,7 @@ export default function AppLayout({ ), }, { - name: 'Create QR', + name: t('nav.create_qr'), href: '/create', icon: ( @@ -48,7 +48,16 @@ export default function AppLayout({ ), }, { - name: 'Analytics', + name: t('nav.bulk_creation'), + href: '/bulk-creation', + icon: ( + + + + ), + }, + { + name: t('nav.analytics'), href: '/analytics', icon: ( @@ -57,7 +66,7 @@ export default function AppLayout({ ), }, { - name: 'Pricing', + name: t('nav.pricing'), href: '/pricing', icon: ( @@ -66,7 +75,7 @@ export default function AppLayout({ ), }, { - name: 'Settings', + name: t('nav.settings'), href: '/settings', icon: ( diff --git a/src/app/(app)/pricing/page.tsx b/src/app/(app)/pricing/page.tsx index c5f91ad..747da75 100644 --- a/src/app/(app)/pricing/page.tsx +++ b/src/app/(app)/pricing/page.tsx @@ -1,357 +1,229 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { showToast } from '@/components/ui/Toast'; +import { useRouter } from 'next/navigation'; export default function PricingPage() { const router = useRouter(); - const searchParams = useSearchParams(); - const [user, setUser] = useState(null); const [loading, setLoading] = useState(null); - const [billingInterval, setBillingInterval] = useState<'monthly' | 'yearly'>('monthly'); - const [hasTriggeredCheckout, setHasTriggeredCheckout] = useState(false); + const [currentPlan, setCurrentPlan] = useState('FREE'); - // Check for user in localStorage useEffect(() => { - const storedUser = localStorage.getItem('user'); - if (storedUser) { - setUser(JSON.parse(storedUser)); - } + // Fetch current user plan + const fetchUserPlan = async () => { + try { + const response = await fetch('/api/user/plan'); + if (response.ok) { + const data = await response.json(); + setCurrentPlan(data.plan || 'FREE'); + } + } catch (error) { + console.error('Error fetching user plan:', error); + } + }; + + fetchUserPlan(); }, []); - const plans = [ - { - id: 'FREE', - name: 'Free / Starter', - icon: '', - price: 0, - priceYearly: 0, - description: 'Privatnutzer & Testkunden', - features: [ - '3 dynamische QR-Codes', - 'Unbegrenzte statische QR-Codes', - 'Basis-Scan-Tracking', - 'Standard QR-Design-Vorlagen', - ], - cta: 'Get Started', - popular: false, - priceIdMonthly: null, - priceIdYearly: null, - }, - { - id: 'PRO', - name: 'Pro', - icon: '', - price: 9, - priceYearly: 90, - description: 'Selbstständige / kleine Firmen', - features: [ - '50 dynamische QR-Codes', - 'Unbegrenzte statische QR-Codes', - 'Erweiterte Analytik (Scans, Geräte, Standorte)', - 'Individuelles Branding (Farben & Logo)', - 'Download als SVG/PNG', - ], - cta: 'Upgrade to Pro', - popular: true, - priceIdMonthly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY, - priceIdYearly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY, - }, - { - id: 'BUSINESS', - name: 'Business', - icon: '', - price: 29, - priceYearly: 290, - description: 'Agenturen / Startups', - features: [ - '500 dynamische QR-Codes', - 'Unbegrenzte statische QR-Codes', - 'Alles aus Pro', - 'Prioritäts-E-Mail-Support', - 'Erweiterte Tracking & Insights', - ], - cta: 'Upgrade to Business', - popular: false, - priceIdMonthly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_BUSINESS_MONTHLY, - priceIdYearly: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_BUSINESS_YEARLY, - }, - ]; - - const handleSubscribe = async (planId: string, priceId: string | null | undefined) => { - console.log('🔵 handleSubscribe called:', { planId, priceId, hasUser: !!user }); - - if (!user) { - // Save the plan selection in localStorage so we can continue after login - const pendingPlan = { - planId, - interval: billingInterval, - }; - console.log('💾 Saving pending plan to localStorage:', pendingPlan); - localStorage.setItem('pendingPlan', JSON.stringify(pendingPlan)); - - // Verify it was saved - const saved = localStorage.getItem('pendingPlan'); - console.log('✅ Verified saved:', saved); - - // Use window.location instead of router.push to ensure localStorage is written - console.log('🔄 Redirecting to login...'); - window.location.href = '/login?redirect=/pricing'; - return; - } - - if (planId === 'FREE') { - showToast('Sie nutzen bereits den kostenlosen Plan!', 'info'); - return; - } - - if (!priceId) { - showToast('Preisdetails nicht verfügbar', 'error'); - return; - } + const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => { + setLoading(plan); try { - setLoading(planId); - - const response = await fetch('/api/stripe/checkout', { + const response = await fetch('/api/stripe/create-checkout-session', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ - priceId, - plan: planId, - userEmail: user.email, + plan, + billingInterval: 'month', }), }); - const data = await response.json(); - - if (response.ok && data.url) { - window.location.href = data.url; - } else { - showToast(data.error || 'Fehler beim Erstellen der Checkout-Session', 'error'); + if (!response.ok) { + throw new Error('Failed to create checkout session'); } + + const { url } = await response.json(); + window.location.href = url; } catch (error) { console.error('Error creating checkout session:', error); - showToast('Ein Fehler ist aufgetreten', 'error'); - } finally { + showToast('Failed to start checkout. Please try again.', 'error'); setLoading(null); } }; - // Auto-trigger checkout after login if plan is selected - useEffect(() => { - console.log('Pricing useEffect triggered:', { - hasUser: !!user, - hasTriggeredCheckout, - }); + const handleDowngrade = async () => { + // Show confirmation dialog + const confirmed = window.confirm( + 'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.' + ); - // Only run once and only when authenticated - if (hasTriggeredCheckout) { - console.log('Already triggered checkout, skipping...'); + if (!confirmed) { return; } - if (!user) { - console.log('Not authenticated - no user in localStorage'); - return; - } + setLoading('FREE'); - // Check for pending plan in localStorage - const pendingPlanStr = localStorage.getItem('pendingPlan'); - if (pendingPlanStr) { - try { - const pendingPlan = JSON.parse(pendingPlanStr); - console.log('✅ Found pending plan:', pendingPlan); + try { + const response = await fetch('/api/stripe/cancel-subscription', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); - // Clear pending plan immediately - localStorage.removeItem('pendingPlan'); - - // Mark as triggered to prevent re-runs - setHasTriggeredCheckout(true); - - // Set the billing interval - setBillingInterval(pendingPlan.interval); - - // Find the plan - const selectedPlan = plans.find((p) => p.id === pendingPlan.planId); - if (selectedPlan) { - const priceId = - pendingPlan.interval === 'yearly' - ? selectedPlan.priceIdYearly - : selectedPlan.priceIdMonthly; - - console.log('✅ Found plan and priceId:', selectedPlan.name, priceId); - - // Trigger checkout after a short delay - setTimeout(() => { - console.log('🚀 Calling handleSubscribe now...'); - handleSubscribe(selectedPlan.id, priceId); - }, 500); - } else { - console.error('❌ Plan not found:', pendingPlan.planId); - } - } catch (e) { - console.error('Error parsing pending plan:', e); - localStorage.removeItem('pendingPlan'); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to cancel subscription'); } - } else { - console.log('No pending plan in localStorage'); + + showToast('Successfully downgraded to Free plan', 'success'); + + // Refresh to update the plan + setTimeout(() => { + window.location.reload(); + }, 1500); + } catch (error: any) { + console.error('Error canceling subscription:', error); + showToast(error.message || 'Failed to downgrade. Please try again.', 'error'); + setLoading(null); } - }, [user, hasTriggeredCheckout]); + }; + + const plans = [ + { + key: 'free', + name: 'Free', + price: '€0', + period: 'forever', + features: [ + '3 dynamic QR codes', + 'Unlimited static QR codes', + 'Basic scan tracking', + 'Standard QR design templates', + ], + buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free', + buttonVariant: 'outline' as const, + disabled: currentPlan === 'FREE', + popular: false, + onDowngrade: handleDowngrade, + }, + { + key: 'pro', + name: 'Pro', + price: '€9', + period: 'per month', + features: [ + '50 dynamic QR codes', + 'Unlimited static QR codes', + 'Advanced analytics (scans, devices, locations)', + 'Custom branding (colors)', + 'Download as SVG/PNG', + ], + buttonText: currentPlan === 'PRO' ? 'Current Plan' : 'Upgrade to Pro', + buttonVariant: 'primary' as const, + disabled: currentPlan === 'PRO', + popular: true, + onUpgrade: () => handleUpgrade('PRO'), + }, + { + key: 'business', + name: 'Business', + price: '€29', + period: 'per month', + features: [ + '500 dynamic QR codes', + 'Unlimited static QR codes', + 'Everything from Pro', + 'Bulk QR Creation (up to 1,000)', + 'Priority email support', + 'Advanced tracking & insights', + ], + buttonText: currentPlan === 'BUSINESS' ? 'Current Plan' : 'Upgrade to Business', + buttonVariant: 'primary' as const, + disabled: currentPlan === 'BUSINESS', + popular: false, + onUpgrade: () => handleUpgrade('BUSINESS'), + }, + ]; return (
-

- Wählen Sie Ihren Plan +

+ Choose Your Plan

-

- Starten Sie kostenlos. Upgraden Sie jederzeit. +

+ Select the perfect plan for your QR code needs

- - {/* Billing Toggle */} -
- - -
- {plans.map((plan) => { - const price = billingInterval === 'yearly' ? plan.priceYearly : plan.price; - const priceId = - billingInterval === 'yearly' ? plan.priceIdYearly : plan.priceIdMonthly; - const isLoading = loading === plan.id; + {plans.map((plan) => ( + + {plan.popular && ( +
+ + Most Popular + +
+ )} - return ( - - {plan.popular && ( -
- - Beliebteste Wahl - -
- )} + + + {plan.name} + +
+ + {plan.price} + + + {plan.period} + +
+
- - {plan.icon &&
{plan.icon}
} - {plan.name} -

{plan.description}

+ +
    + {plan.features.map((feature: string, index: number) => ( +
  • + + + + {feature} +
  • + ))} +
-
-
- {price}€ - - /{billingInterval === 'yearly' ? 'Jahr' : 'Monat'} - -
- {billingInterval === 'yearly' && plan.price > 0 && ( -

- {(price / 12).toFixed(2)}€ pro Monat -

- )} -
-
- - -
    - {plan.features.map((feature, index) => ( -
  • - - - - {feature} -
  • - ))} -
- - -
-
- ); - })} + + +
+ ))}
- {/* FAQ Section */} -
-

Häufige Fragen

-
- - -

Kann ich jederzeit kündigen?

-

- Ja, Sie können Ihr Abo jederzeit kündigen. Es läuft dann bis zum Ende des - bezahlten Zeitraums weiter. -

-
-
- - - -

Welche Zahlungsmethoden akzeptieren Sie?

-

- Wir akzeptieren alle gängigen Kreditkarten und SEPA-Lastschrift über Stripe. -

-
-
- - - -

Was passiert mit meinen QR-Codes bei Downgrade?

-

- Ihre QR-Codes bleiben erhalten, Sie können nur keine neuen mehr erstellen, wenn das Limit erreicht ist. -

-
-
-
+
+

+ All plans include unlimited static QR codes and basic customization. +

+

+ Need help choosing? Contact our team +

); diff --git a/src/app/(app)/qr/[id]/edit/page.tsx b/src/app/(app)/qr/[id]/edit/page.tsx new file mode 100644 index 0000000..abdbb05 --- /dev/null +++ b/src/app/(app)/qr/[id]/edit/page.tsx @@ -0,0 +1,210 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { showToast } from '@/components/ui/Toast'; + +export default function EditQRPage() { + const router = useRouter(); + const params = useParams(); + const qrId = params.id as string; + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [title, setTitle] = useState(''); + const [content, setContent] = useState({}); + + useEffect(() => { + const fetchQRCode = async () => { + try { + const response = await fetch(`/api/qrs/${qrId}`); + if (response.ok) { + const data = await response.json(); + setQrCode(data); + setTitle(data.title); + setContent(data.content || {}); + } else { + showToast('Failed to load QR code', 'error'); + router.push('/dashboard'); + } + } catch (error) { + console.error('Error fetching QR code:', error); + showToast('Failed to load QR code', 'error'); + router.push('/dashboard'); + } finally { + setLoading(false); + } + }; + + fetchQRCode(); + }, [qrId, router]); + + const handleSave = async () => { + setSaving(true); + + try { + const response = await fetch(`/api/qrs/${qrId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title, + content, + }), + }); + + if (response.ok) { + showToast('QR code updated successfully!', 'success'); + router.push('/dashboard'); + } else { + const error = await response.json(); + showToast(error.error || 'Failed to update QR code', 'error'); + } + } catch (error) { + console.error('Error updating QR code:', error); + showToast('Failed to update QR code', 'error'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+
+

Loading QR code...

+
+
+ ); + } + + if (!qrCode) { + return null; + } + + // Static QR codes cannot be edited + if (qrCode.type === 'STATIC') { + return ( +
+ + +
+ + + +
+

Static QR Code

+

+ Static QR codes cannot be edited because their content is embedded directly in the QR code image. +

+ +
+
+
+ ); + } + + return ( +
+
+

Edit QR Code

+

Update your dynamic QR code content

+
+ + + + QR Code Details + + + setTitle(e.target.value)} + placeholder="Enter QR code title" + required + /> + + {qrCode.contentType === 'URL' && ( + setContent({ ...content, url: e.target.value })} + placeholder="https://example.com" + required + /> + )} + + {qrCode.contentType === 'PHONE' && ( + setContent({ ...content, phone: e.target.value })} + placeholder="+1234567890" + required + /> + )} + + {qrCode.contentType === 'EMAIL' && ( + <> + setContent({ ...content, email: e.target.value })} + placeholder="email@example.com" + required + /> + setContent({ ...content, subject: e.target.value })} + placeholder="Email subject" + /> + + )} + + {qrCode.contentType === 'TEXT' && ( +
+ +