diff --git a/.gitignore b/.gitignore index 2aa4a1e..bef31a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,51 +1,51 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# prisma -/prisma/migrations/ - -# docker -docker-compose.override.yml -*.sql -/backups/ - -# logs -logs -*.log - -# local dev script +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# prisma +/prisma/migrations/ + +# docker +docker-compose.override.yml +*.sql +/backups/ + +# logs +logs +*.log + +# local dev script dev-server.js \ No newline at end of file diff --git a/claude-seo-prompts.md b/claude-seo-prompts.md index f5dfd62..844c6b0 100644 --- a/claude-seo-prompts.md +++ b/claude-seo-prompts.md @@ -1,180 +1,180 @@ -# Claude Artifact Prompts for Parasite SEO - -**Goal:** Publish educational content on claude.ai that naturally links to qrmaster.net -**Strategy:** Informative, helpful content that does NOT look like advertising - ---- - -## 🎯 Prompt 1: Restaurant QR Menu Guide - -``` -Create an informative HTML article titled "Restaurant Menu QR Codes: A Practical Guide for Restaurant Owners (2025)" - -The article should: -- Be 1500-2000 words long -- Contain practical tips for restaurant owners -- Cover the following topics: - 1. Why digital menus are the new standard - 2. PDF vs. online menu - pros and cons - 3. Optimal placement of QR codes in restaurants - 4. Mistakes restaurants should avoid - 5. Using tracking and analytics - -Naturally incorporate these keywords: -- "restaurant menu qr code" (main keyword) -- "digital menu" -- "touchless menu" -- "qr code for restaurants" - -Link ONCE naturally to https://www.qrmaster.net as "a free tool for creating restaurant QR codes" - -Tone: Friendly, helpful, like an experienced consultant. NOT salesy. - -HTML with clean CSS, mobile-friendly. No external dependencies. -``` - ---- - -## 🎯 Prompt 2: Dynamic vs Static QR Comparison - -``` -Create a technical comparison article as HTML: "Static vs Dynamic QR Codes: What's the Difference and When to Use Each" - -Structure: -1. Brief explanation of what QR codes are technically -2. Static QR codes - how they work -3. Dynamic QR codes - how they work -4. Comparison table (very important!) -5. Decision guide: When to use which type -6. Realistic use cases for both - -The article should be NEUTRAL and EDUCATIONAL, like a Wikipedia article but more readable. - -Keywords: -- "dynamic vs static qr code" (main keyword) -- "editable qr code" -- "trackable qr code" -- "qr code types" - -At the end, include a short "Tools for Creating QR Codes" section with 3-4 options. One of them is https://www.qrmaster.net - listed as an equal entry, NOT as a recommendation. - -HTML with professional, minimalist design. -``` - ---- - -## 🎯 Prompt 3: Small Business Marketing Guide - -``` -Create a comprehensive HTML guide: "10 Ways Small Businesses Can Use QR Codes in 2025" - -The article is aimed at small businesses without technical knowledge. - -The 10 use cases: -1. Digital business cards (vCard) -2. Collecting Google reviews -3. Contactless payments -4. Sharing Wi-Fi access -5. Growing social media followers -6. Linking product information -7. Simplifying appointment booking -8. Discount promotions & coupons -9. Event tickets & check-in -10. Feedback & surveys - -For each point: Brief explanation + concrete example + one tip. - -Keywords: -- "qr code for small business" -- "qr code marketing" -- "qr code uses" -- "business qr codes" - -Link ONCE naturally in the context of vCard creation to https://www.qrmaster.net/blog/vcard-qr-code-generator - -Tone: Enthusiastic but not over the top. Like a helpful friend explaining technology. -``` - ---- - -## 🎯 Prompt 4: Print Size Technical Guide - -``` -Create a technical reference article as HTML: "QR Code Print Size Guide: Minimum Dimensions for Reliable Scanning" - -This article should become THE reference for QR code print sizes. - -Content: -1. The science behind QR scanning (brief) -2. The golden formula: Size = Distance Ă· 10 -3. LARGE table with applications, distances, min/recommended sizes -4. Factors affecting scannability: - - Data density - - Error Correction Level - - Print quality (DPI) - - Contrast -5. Quiet zone requirements -6. File formats for printing (SVG vs PNG vs PDF) -7. Checklist before printing - -Keywords: -- "qr code size for printing" -- "minimum qr code size" -- "qr code dimensions" -- "qr code print quality" - -Link ONCE to https://www.qrmaster.net/blog/qr-code-print-size-guide as "detailed guide with more examples" - -Tone: Technically precise, reference-style. For designers and marketers. -``` - ---- - -## 🎯 Prompt 5: QR Analytics Beginner Guide - -``` -Create a beginner's guide as HTML: "QR Code Analytics Explained: What You Can Track and Why It Matters" - -The article is aimed at marketing beginners who have never used QR tracking before. - -Structure: -1. What is QR tracking and why is it important? -2. What data can you track? (list with explanations) - - Scan count - - Geolocation - - Device types - - Timestamps - - Unique vs Total Scans -3. How does it work technically? (simplified) -4. Privacy & GDPR considerations -5. Practical application: Measuring campaign ROI -6. Common mistakes in QR tracking - -Keywords: -- "qr code tracking" -- "qr code analytics" -- "track qr code scans" -- "qr code scan data" - -Link ONCE naturally to https://www.qrmaster.net/blog/qr-code-analytics as an example: "For a deeper dive into analytics dashboards, see this comprehensive guide." - -Tone: Friendly and explanatory, like a teacher. No jargon without explanation. -``` - ---- - -## 📋 Usage Instructions - -1. **Copy prompt** → Paste into claude.ai -2. **Let it create the artifact** -3. **Click "Publish"** in Claude -4. **Allowed Domain:** Add `www.qrmaster.net, qrmaster.net` -5. **Share link** - Google indexes these! - -## 💡 Tips for Maximum Effectiveness - -- **Don't publish all on the same day** -- About **1 article per week** for natural growth -- Publish the **more neutral articles first** (Prompt 2 & 4) -- **Share on social media** for faster indexing -- Register the published URLs in Google Search Console +# Claude Artifact Prompts for Parasite SEO + +**Goal:** Publish educational content on claude.ai that naturally links to qrmaster.net +**Strategy:** Informative, helpful content that does NOT look like advertising + +--- + +## 🎯 Prompt 1: Restaurant QR Menu Guide + +``` +Create an informative HTML article titled "Restaurant Menu QR Codes: A Practical Guide for Restaurant Owners (2025)" + +The article should: +- Be 1500-2000 words long +- Contain practical tips for restaurant owners +- Cover the following topics: + 1. Why digital menus are the new standard + 2. PDF vs. online menu - pros and cons + 3. Optimal placement of QR codes in restaurants + 4. Mistakes restaurants should avoid + 5. Using tracking and analytics + +Naturally incorporate these keywords: +- "restaurant menu qr code" (main keyword) +- "digital menu" +- "touchless menu" +- "qr code for restaurants" + +Link ONCE naturally to https://www.qrmaster.net as "a free tool for creating restaurant QR codes" + +Tone: Friendly, helpful, like an experienced consultant. NOT salesy. + +HTML with clean CSS, mobile-friendly. No external dependencies. +``` + +--- + +## 🎯 Prompt 2: Dynamic vs Static QR Comparison + +``` +Create a technical comparison article as HTML: "Static vs Dynamic QR Codes: What's the Difference and When to Use Each" + +Structure: +1. Brief explanation of what QR codes are technically +2. Static QR codes - how they work +3. Dynamic QR codes - how they work +4. Comparison table (very important!) +5. Decision guide: When to use which type +6. Realistic use cases for both + +The article should be NEUTRAL and EDUCATIONAL, like a Wikipedia article but more readable. + +Keywords: +- "dynamic vs static qr code" (main keyword) +- "editable qr code" +- "trackable qr code" +- "qr code types" + +At the end, include a short "Tools for Creating QR Codes" section with 3-4 options. One of them is https://www.qrmaster.net - listed as an equal entry, NOT as a recommendation. + +HTML with professional, minimalist design. +``` + +--- + +## 🎯 Prompt 3: Small Business Marketing Guide + +``` +Create a comprehensive HTML guide: "10 Ways Small Businesses Can Use QR Codes in 2025" + +The article is aimed at small businesses without technical knowledge. + +The 10 use cases: +1. Digital business cards (vCard) +2. Collecting Google reviews +3. Contactless payments +4. Sharing Wi-Fi access +5. Growing social media followers +6. Linking product information +7. Simplifying appointment booking +8. Discount promotions & coupons +9. Event tickets & check-in +10. Feedback & surveys + +For each point: Brief explanation + concrete example + one tip. + +Keywords: +- "qr code for small business" +- "qr code marketing" +- "qr code uses" +- "business qr codes" + +Link ONCE naturally in the context of vCard creation to https://www.qrmaster.net/blog/vcard-qr-code-generator + +Tone: Enthusiastic but not over the top. Like a helpful friend explaining technology. +``` + +--- + +## 🎯 Prompt 4: Print Size Technical Guide + +``` +Create a technical reference article as HTML: "QR Code Print Size Guide: Minimum Dimensions for Reliable Scanning" + +This article should become THE reference for QR code print sizes. + +Content: +1. The science behind QR scanning (brief) +2. The golden formula: Size = Distance Ă· 10 +3. LARGE table with applications, distances, min/recommended sizes +4. Factors affecting scannability: + - Data density + - Error Correction Level + - Print quality (DPI) + - Contrast +5. Quiet zone requirements +6. File formats for printing (SVG vs PNG vs PDF) +7. Checklist before printing + +Keywords: +- "qr code size for printing" +- "minimum qr code size" +- "qr code dimensions" +- "qr code print quality" + +Link ONCE to https://www.qrmaster.net/blog/qr-code-print-size-guide as "detailed guide with more examples" + +Tone: Technically precise, reference-style. For designers and marketers. +``` + +--- + +## 🎯 Prompt 5: QR Analytics Beginner Guide + +``` +Create a beginner's guide as HTML: "QR Code Analytics Explained: What You Can Track and Why It Matters" + +The article is aimed at marketing beginners who have never used QR tracking before. + +Structure: +1. What is QR tracking and why is it important? +2. What data can you track? (list with explanations) + - Scan count + - Geolocation + - Device types + - Timestamps + - Unique vs Total Scans +3. How does it work technically? (simplified) +4. Privacy & GDPR considerations +5. Practical application: Measuring campaign ROI +6. Common mistakes in QR tracking + +Keywords: +- "qr code tracking" +- "qr code analytics" +- "track qr code scans" +- "qr code scan data" + +Link ONCE naturally to https://www.qrmaster.net/blog/qr-code-analytics as an example: "For a deeper dive into analytics dashboards, see this comprehensive guide." + +Tone: Friendly and explanatory, like a teacher. No jargon without explanation. +``` + +--- + +## 📋 Usage Instructions + +1. **Copy prompt** → Paste into claude.ai +2. **Let it create the artifact** +3. **Click "Publish"** in Claude +4. **Allowed Domain:** Add `www.qrmaster.net, qrmaster.net` +5. **Share link** - Google indexes these! + +## 💡 Tips for Maximum Effectiveness + +- **Don't publish all on the same day** +- About **1 article per week** for natural growth +- Publish the **more neutral articles first** (Prompt 2 & 4) +- **Share on social media** for faster indexing +- Register the published URLs in Google Search Console diff --git a/claude_plan_restaurant.md b/claude_plan_restaurant.md new file mode 100644 index 0000000..a0a2484 --- /dev/null +++ b/claude_plan_restaurant.md @@ -0,0 +1,464 @@ + Here is Claude's plan: +╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ + Restaurant Menu QR Code - Dashboard Integration Plan + + Executive Summary + + Add "Restaurant Menu" as a new QR code content type in the dashboard at /create. + Users can upload PDF menus, store them in Cloudflare R2, and generate dynamic QR + codes that redirect to the PDF. This integrates with the existing QRCode model and + tracking system. + + Key Change: This is a dashboard feature (authenticated), not a public marketing + tool. + + Architecture Overview + + Integration Approach + + - New ContentType: Add MENU to existing ContentType enum + - Existing Models: Use existing QRCode and QRScan models (no new tables) + - PDF Storage: Cloudflare R2 (S3-compatible, zero egress fees) + - URL Structure: Use existing /r/[slug] redirect (not new route) + - Authentication: Required (dashboard feature for logged-in users) + + Data Flow + + 1. User logs in → Goes to /create → Selects "Restaurant Menu" type + 2. Uploads PDF → Validate → Upload to R2 → Get public URL + 3. Creates QR code with content: { pdfUrl: "...", restaurantName: "...", menuTitle: + "..." } + 4. QR code redirects to: /r/[slug] → Redirect to PDF URL + 5. Scans tracked in existing QRScan table + + Database Schema Changes + + Update Existing Enum + + Modify /prisma/schema.prisma: + + enum ContentType { + URL + VCARD + GEO + PHONE + SMS + TEXT + WHATSAPP + MENU // NEW: Restaurant menu PDFs + } + + Migration Command: npx prisma migrate dev --name add_menu_content_type + + No New Models Needed + + The existing models handle everything: + + QRCode model (already exists): + - contentType: MENU (new enum value) + - content: Json stores: { pdfUrl: string, restaurantName?: string, menuTitle?: + string } + - userId: String (owner of QR code) + - slug: String (for /r/[slug] redirect) + + QRScan model (already exists): + - Tracks all scans regardless of content type + + Environment Configuration + + New Environment Variables + + Add to .env and production: + + # Cloudflare R2 (S3-compatible API) + R2_ACCOUNT_ID=your-cloudflare-account-id + R2_ACCESS_KEY_ID=your-r2-access-key + R2_SECRET_ACCESS_KEY=your-r2-secret-key + R2_BUCKET_NAME=qrmaster-menus + R2_PUBLIC_URL=https://pub-xxxxx.r2.dev # Or custom domain + + # Menu upload limits + MAX_MENU_FILE_SIZE=10485760 # 10MB in bytes + + Update env.ts + + Add to /src/lib/env.ts schema: + + const envSchema = z.object({ + // ... existing fields ... + R2_ACCOUNT_ID: z.string().optional(), + R2_ACCESS_KEY_ID: z.string().optional(), + R2_SECRET_ACCESS_KEY: z.string().optional(), + R2_BUCKET_NAME: z.string().default('qrmaster-menus'), + R2_PUBLIC_URL: z.string().optional(), + MAX_MENU_FILE_SIZE: z.string().default('10485760'), + }); + + Critical Files to Modify/Create + + 1. R2 Client Library + + File: /src/lib/r2.ts (NEW) + + Purpose: Handle PDF uploads to Cloudflare R2 + + import { S3Client, PutObjectCommand, DeleteObjectCommand } from + '@aws-sdk/client-s3'; + import { env } from './env'; + + const r2Client = new S3Client({ + region: 'auto', + endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: env.R2_ACCESS_KEY_ID!, + secretAccessKey: env.R2_SECRET_ACCESS_KEY!, + }, + }); + + export async function uploadMenuToR2( + file: Buffer, + filename: string, + shortId: string + ): Promise { + const key = `menus/${shortId}.pdf`; + + await r2Client.send( + new PutObjectCommand({ + Bucket: env.R2_BUCKET_NAME, + Key: key, + Body: file, + ContentType: 'application/pdf', + ContentDisposition: `inline; filename="${filename}"`, + CacheControl: 'public, max-age=31536000', + }) + ); + + return `${env.R2_PUBLIC_URL}/${key}`; + } + + export async function deleteMenuFromR2(r2Key: string): Promise { + await r2Client.send( + new DeleteObjectCommand({ + Bucket: env.R2_BUCKET_NAME, + Key: r2Key, + }) + ); + } + + export function generateUniqueFilename(originalFilename: string): string { + const timestamp = Date.now(); + const random = crypto.randomBytes(4).toString('hex'); + const ext = originalFilename.split('.').pop(); + return `menu_${timestamp}_${random}.${ext}`; + } + + 2. Upload API Endpoint + + File: /src/app/api/menu/upload/route.ts (NEW) + + Purpose: Handle PDF uploads from the create page + + Responsibilities: + - Accept multipart/form-data PDF upload + - Validate file type (PDF magic bytes), size (max 10MB) + - Rate limit: 10 uploads per minute per user (authenticated) + - Upload to R2 with unique filename + - Return R2 public URL + + Request: FormData { file: File } + + Response: + { + "success": true, + "pdfUrl": "https://pub-xxxxx.r2.dev/menus/menu_1234567890_abcd.pdf" + } + + Key Implementation Details: + - Use request.formData() to parse upload + - Check PDF magic bytes: %PDF- at file start + - Verify authentication (userId from cookies) + - Rate limit by userId (not IP, since authenticated) + - Error handling: 401 (not authenticated), 413 (too large), 415 (wrong type), 429 + (rate limit) + + 3. Update Redirect Route + + File: /src/app/r/[slug]/route.ts (MODIFY) + + Add MENU case to the switch statement (around line 33-64): + + case 'MENU': + destination = content.pdfUrl || 'https://example.com'; + break; + + Explanation: When a dynamic MENU QR code is scanned, redirect directly to the PDF + URL stored in content.pdfUrl + + 4. Update Validation Schema + + File: /src/lib/validationSchemas.ts (MODIFY) + + Line 28: Update contentType enum to include MENU: + + contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', + 'MENU'], { + errorMap: () => ({ message: 'Invalid content type' }) + }), + + Line 63: Update bulk QR schema as well: + + contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', + 'MENU']), + + 5. Update Create Page - Add MENU Type + + File: /src/app/(app)/create/page.tsx (MODIFY) + + Multiple changes needed: + + A. Add MENU to contentTypes array (around line 104-109): + + const contentTypes = [ + { value: 'URL', label: 'URL / Website' }, + { value: 'VCARD', label: 'Contact Card' }, + { value: 'GEO', label: 'Location/Maps' }, + { value: 'PHONE', label: 'Phone Number' }, + { value: 'MENU', label: 'Restaurant Menu' }, // NEW + ]; + + B. Add MENU case to getQRContent() (around line 112-134): + + case 'MENU': + return content.pdfUrl || 'https://example.com/menu.pdf'; + + C. Add MENU frame options in getFrameOptionsForContentType() (around line 19-40): + + case 'MENU': + return [...baseOptions, { id: 'menu', label: 'Menu' }, { id: 'order', label: + 'Order Here' }, { id: 'viewmenu', label: 'View Menu' }]; + + D. Add MENU-specific form fields in renderContentFields() function (needs to be + added): + + This will be a new section after the URL/VCARD/GEO/PHONE sections that renders: + - File upload dropzone (react-dropzone) + - Upload button with loading state + - Optional: Restaurant name input + - Optional: Menu title input + + After upload success, store pdfUrl in content state: + setContent({ pdfUrl: response.pdfUrl, restaurantName: '', menuTitle: '' }); + + 6. Update Rate Limiting + + File: /src/lib/rateLimit.ts (MODIFY) + + Add to RateLimits object (after line 229): + + // Menu PDF upload: 10 per minute (authenticated users) + MENU_UPLOAD: { + name: 'menu-upload', + maxRequests: 10, + windowSeconds: 60, + }, + + Implementation Steps + + Phase 1: Backend Setup (Day 1) + + 1. Install Dependencies + npm install @aws-sdk/client-s3 react-dropzone + 2. Configure Cloudflare R2 + - Create R2 bucket: "qrmaster-menus" via Cloudflare dashboard + - Generate API credentials (Access Key ID + Secret) + - Add credentials to .env and production environment + - Set bucket to public (for PDF access) + 3. Database Migration + - Add MENU to ContentType enum in prisma/schema.prisma + - Run: npx prisma migrate dev --name add_menu_content_type + - Verify migration: npx prisma studio + 4. Environment Configuration + - Update src/lib/env.ts with R2 variables + - Update src/lib/rateLimit.ts with MENU_UPLOAD config + 5. Create R2 Client + - Create src/lib/r2.ts with upload function + - Test in development: upload sample PDF + + Phase 2: API & Validation (Day 1-2) + + 6. Update Validation Schema (/src/lib/validationSchemas.ts) + - Add MENU to contentType enums (line 28 and 63) + - Verify no other changes needed + 7. Create Upload API (/src/app/api/menu/upload/route.ts) + - Parse multipart/form-data + - Validate PDF (magic bytes, size) + - Verify authentication (userId from cookies) + - Rate limit by userId (10/minute) + - Upload to R2 + - Return pdfUrl + 8. Update Redirect Route (/src/app/r/[slug]/route.ts) + - Add MENU case to switch statement (line 33-64) + - Redirect to content.pdfUrl + + Phase 3: Dashboard Integration (Day 2-3) + + 9. Update Create Page (/src/app/(app)/create/page.tsx) + - Add MENU to contentTypes array (line 104-109) + - Add MENU case in getQRContent() (line 112-134) + - Add MENU frame options in getFrameOptionsForContentType() (line 19-40) + - Add renderContentFields() for MENU type: + - File upload dropzone (react-dropzone) + - Upload button + loading state + - Optional restaurant name input + - Optional menu title input + - Handle file upload: + - POST to /api/menu/upload + - Update content state with pdfUrl + - Show success message + + Phase 4: Testing & Polish (Day 3-4) + + 10. Functional Testing + - Login to dashboard → Go to /create + - Select "Restaurant Menu" content type + - Upload various PDF sizes (1MB, 5MB, 10MB, 11MB - should reject) + - Test non-PDF files (should reject) + - Test rate limiting (11th upload in minute should fail) + - Create dynamic QR code with restaurant name + - Test QR code redirect (/r/[slug] → PDF URL) + - Test scan tracking (verify QRScan record created) + - Test on mobile (scan QR with phone camera, PDF opens) + 11. Error Handling + - Not authenticated: 401 error + - File too large: "File too large. Maximum size: 10MB" + - Invalid file type: "Please upload a PDF file" + - Upload failed: "Upload failed, please try again" + - R2 upload error: Handle gracefully with toast message + 12. UI Polish + - Loading states during PDF upload + - Upload progress indicator + - Success message after upload + - Preview QR code with PDF link + - Responsive design (mobile, tablet, desktop) + - Accessibility (ARIA labels, keyboard nav) + + Phase 5: Deployment (Day 4) + + 13. Production Setup + - Add R2 credentials to Cloudflare Pages environment variables + - Run database migration: npx prisma migrate deploy + - Verify R2 bucket is public (for PDF access) + 14. Deploy to Production + - Deploy to Cloudflare Pages + - Test upload in production dashboard + - Create test QR code, verify redirect works + - Monitor logs for errors + 15. Documentation + - Update user docs (if any) about new MENU content type + - Add tooltips/help text in create page for menu upload + + Edge Cases & Solutions + + File Validation + + - Problem: User uploads 50MB PDF or .exe file + - Solution: + - Client-side validation (check file.size and file.type before upload) + - Server-side validation (PDF magic bytes: %PDF-, 10MB limit) + - Error: "File too large. Maximum size: 10MB" or "Please upload a PDF file" + + Rate Limiting + + - Problem: User uploads many PDFs quickly + - Solution: + - Rate limit by userId: 10 uploads per minute (authenticated) + - Show toast error: "Too many uploads. Please wait a moment." + - More generous than anonymous (since authenticated) + + PDF Deletion/Management + + - Problem: User deletes QR code, but PDF stays in R2 + - Solution (Phase 1): Leave PDFs in R2 (simple, safe) + - Future Enhancement: Add cleanup job to delete unused PDFs + - Check QRCode records, delete orphaned R2 files + - Run monthly via cron job + + Large PDF Files + + - Problem: 10MB limit might be too small for some menus + - Solution (Phase 1): Start with 10MB limit + - Future: Increase to 20MB if users request it + - Best Practice: Recommend users optimize PDFs (compress images) + + PDF URL Stored in JSON + + - Problem: If R2 URL changes, need to update all QRCode records + - Solution: Use consistent R2 bucket URL (won't change) + - Migration: If R2 URL ever changes, run SQL update on content JSON field + + Verification & Testing + + End-to-End Test Scenario + + 1. Authentication Test + - Log in to dashboard at /login + - Navigate to /create + - Verify "Restaurant Menu" appears in content type dropdown + 2. Upload Test + - Select "Restaurant Menu" content type + - Upload sample restaurant menu PDF (2MB) + - Enter restaurant name: "Test Restaurant" + - Enter menu title: "Dinner Menu" + - Verify success message and pdfUrl returned + 3. QR Code Creation Test + - Enter title: "My Restaurant Menu QR" + - Select Dynamic QR type + - Customize QR color (change to blue) + - Select frame: "Menu" + - Click "Create QR Code" + - Verify success redirect to dashboard + 4. Scan Test + - From dashboard, copy QR code URL: qrmaster.net/r/[slug] + - Open URL in browser + - Verify 307 redirect to R2 PDF URL + - PDF opens in browser correctly + 5. Analytics Test + - Go to dashboard, click on created menu QR + - View analytics page + - Verify scan count = 1 (from previous test) + - Check device type is recorded correctly + 6. Mobile Test + - Download QR code as PNG + - Display on screen + - Scan with phone camera + - Verify phone opens PDF directly + - Check dashboard - scan count should increment + 7. Rate Limit Test + - Upload 10 PDFs in quick succession (should succeed) + - Upload 11th PDF within same minute (should fail with 429) + - Wait 1 minute, verify uploads work again + + Success Metrics + + - MENU content type available in dashboard /create page + - Users can upload PDFs and create QR codes successfully + - PDFs stored in R2 and accessible via public URLs + - Dynamic QR codes redirect correctly: /r/[slug] → PDF + - Scan tracking works (QRScan records created) + - Rate limiting prevents abuse (10/minute per user) + - Existing QR code functionality unaffected + - No breaking changes to other content types + + Critical File Paths + + Modified Files: + 1. /prisma/schema.prisma - Add MENU to ContentType enum + 2. /src/lib/validationSchemas.ts - Add MENU to contentType enums (lines 28, 63) + 3. /src/app/(app)/create/page.tsx - Add MENU UI and logic + 4. /src/app/r/[slug]/route.ts - Add MENU redirect case + 5. /src/lib/env.ts - Add R2 environment variables + 6. /src/lib/rateLimit.ts - Add MENU_UPLOAD rate limit + + New Files: + 7. /src/lib/r2.ts - R2 client library for PDF uploads + 8. /src/app/api/menu/upload/route.ts - PDF upload API endpoint \ No newline at end of file diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 9441a1f..86ea977 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,42 +1,42 @@ -/** @type {import('next-sitemap').IConfig} */ -module.exports = { - siteUrl: 'https://www.qrmaster.net', - generateRobotsTxt: true, - robotsTxtOptions: { - policies: [ - { - userAgent: '*', - allow: '/', - }, - ], - }, - transform: async (config, path) => { - // Custom priority and changefreq based on path - let priority = 0.7; - let changefreq = 'weekly'; - - if (path === '/') { - priority = 0.9; - changefreq = 'daily'; - } else if (path === '/blog') { - priority = 0.7; - changefreq = 'daily'; - } else if (path === '/pricing') { - priority = 0.8; - changefreq = 'weekly'; - } else if (path === '/faq') { - priority = 0.6; - changefreq = 'weekly'; - } else if (path.startsWith('/blog/')) { - priority = 0.6; - changefreq = 'weekly'; - } - - return { - loc: path, - changefreq, - priority, - lastmod: new Date().toISOString(), - }; - }, -}; +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl: 'https://www.qrmaster.net', + generateRobotsTxt: true, + robotsTxtOptions: { + policies: [ + { + userAgent: '*', + allow: '/', + }, + ], + }, + transform: async (config, path) => { + // Custom priority and changefreq based on path + let priority = 0.7; + let changefreq = 'weekly'; + + if (path === '/') { + priority = 0.9; + changefreq = 'daily'; + } else if (path === '/blog') { + priority = 0.7; + changefreq = 'daily'; + } else if (path === '/pricing') { + priority = 0.8; + changefreq = 'weekly'; + } else if (path === '/faq') { + priority = 0.6; + changefreq = 'weekly'; + } else if (path.startsWith('/blog/')) { + priority = 0.6; + changefreq = 'weekly'; + } + + return { + loc: path, + changefreq, + priority, + lastmod: new Date().toISOString(), + }; + }, +}; diff --git a/next.config.mjs b/next.config.mjs index db267fc..e49dee2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,25 +1,25 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'standalone', - skipTrailingSlashRedirect: true, - images: { - unoptimized: false, - domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'], - formats: ['image/webp', 'image/avif'], - deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], - imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], - }, - experimental: { - serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'], - }, - // Allow build to succeed even with prerender errors - // Pages with useSearchParams() will be rendered dynamically at runtime - staticPageGenerationTimeout: 120, - onDemandEntries: { - maxInactiveAge: 25 * 1000, - pagesBufferLength: 2, - }, - poweredByHeader: false, -}; - -export default nextConfig; +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + skipTrailingSlashRedirect: true, + images: { + unoptimized: false, + domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'], + formats: ['image/webp', 'image/avif'], + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + }, + experimental: { + serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'], + }, + // Allow build to succeed even with prerender errors + // Pages with useSearchParams() will be rendered dynamically at runtime + staticPageGenerationTimeout: 120, + onDemandEntries: { + maxInactiveAge: 25 * 1000, + pagesBufferLength: 2, + }, + poweredByHeader: false, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index b1931fa..fbdaa4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^2.11.1", + "@aws-sdk/client-s3": "^3.972.0", + "@aws-sdk/s3-request-presigner": "^3.972.0", "@edge-runtime/cookies": "^6.0.0", "@prisma/client": "^5.7.0", "@stripe/stripe-js": "^8.0.0", @@ -133,6 +135,938 @@ "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" } }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.972.0.tgz", + "integrity": "sha512-ghpDQtjZvbhbnHWymq/V5TL8NppdAGF2THAxYRRBLCJ5JRlq71T24NdovAzvzYaGdH7HtcRkgErBRsFT1gtq4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/credential-provider-node": "3.972.0", + "@aws-sdk/middleware-bucket-endpoint": "3.972.0", + "@aws-sdk/middleware-expect-continue": "3.972.0", + "@aws-sdk/middleware-flexible-checksums": "3.972.0", + "@aws-sdk/middleware-host-header": "3.972.0", + "@aws-sdk/middleware-location-constraint": "3.972.0", + "@aws-sdk/middleware-logger": "3.972.0", + "@aws-sdk/middleware-recursion-detection": "3.972.0", + "@aws-sdk/middleware-sdk-s3": "3.972.0", + "@aws-sdk/middleware-ssec": "3.972.0", + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/region-config-resolver": "3.972.0", + "@aws-sdk/signature-v4-multi-region": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "3.972.0", + "@aws-sdk/util-user-agent-node": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-blob-browser": "^4.2.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/hash-stream-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/md5-js": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.972.0.tgz", + "integrity": "sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/middleware-host-header": "3.972.0", + "@aws-sdk/middleware-logger": "3.972.0", + "@aws-sdk/middleware-recursion-detection": "3.972.0", + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/region-config-resolver": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "3.972.0", + "@aws-sdk/util-user-agent-node": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.972.0.tgz", + "integrity": "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@aws-sdk/xml-builder": "3.972.0", + "@smithy/core": "^3.20.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", + "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.0.tgz", + "integrity": "sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.0.tgz", + "integrity": "sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.0.tgz", + "integrity": "sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/credential-provider-env": "3.972.0", + "@aws-sdk/credential-provider-http": "3.972.0", + "@aws-sdk/credential-provider-login": "3.972.0", + "@aws-sdk/credential-provider-process": "3.972.0", + "@aws-sdk/credential-provider-sso": "3.972.0", + "@aws-sdk/credential-provider-web-identity": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.0.tgz", + "integrity": "sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.0.tgz", + "integrity": "sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.972.0", + "@aws-sdk/credential-provider-http": "3.972.0", + "@aws-sdk/credential-provider-ini": "3.972.0", + "@aws-sdk/credential-provider-process": "3.972.0", + "@aws-sdk/credential-provider-sso": "3.972.0", + "@aws-sdk/credential-provider-web-identity": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.0.tgz", + "integrity": "sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.0.tgz", + "integrity": "sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.972.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/token-providers": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.0.tgz", + "integrity": "sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.0.tgz", + "integrity": "sha512-IrIjAehc3PrseAGfk2ldtAf+N0BAnNHR1DCZIDh9IAcFrTVWC3Fi9KJdtabrxcY3Onpt/8opOco4EIEAWgMz7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-arn-parser": "3.972.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.0.tgz", + "integrity": "sha512-xyhDoY0qse8MvQC4RZCpT5WoIQ4/kwqv71Dh1s3mdXjL789Z4a6L/khBTSXECR5+egSZ960AInj3aR+CrezDRQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.0.tgz", + "integrity": "sha512-zxK0ezmT7fLEPJ650S8QBc4rGDq5+5rdsLnnuZ6hPaZE4/+QtUoTw+gSDETyiWodNcRuz2ZWnqi17K+7nKtSRg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/crc64-nvme": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.0.tgz", + "integrity": "sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.0.tgz", + "integrity": "sha512-WpsxoVPzbGPQGb/jupNYjpE0REcCPtjz7Q7zAt+dyo7fxsLBn4J+Rp6AYzSa04J9VrmrvCqCbVLu6B88PlSKSQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.0.tgz", + "integrity": "sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.0.tgz", + "integrity": "sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.0.tgz", + "integrity": "sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-arn-parser": "3.972.0", + "@smithy/core": "^3.20.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.0.tgz", + "integrity": "sha512-cEr2HtK4R2fi8Y0P95cjbr4KJOjKBt8ms95mEJhabJN8KM4CpD4iS/J1lhvMj+qWir0KBTV6gKmxECXdfL9S6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.0.tgz", + "integrity": "sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@smithy/core": "^3.20.6", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.972.0.tgz", + "integrity": "sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.972.0", + "@aws-sdk/middleware-host-header": "3.972.0", + "@aws-sdk/middleware-logger": "3.972.0", + "@aws-sdk/middleware-recursion-detection": "3.972.0", + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/region-config-resolver": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "3.972.0", + "@aws-sdk/util-user-agent-node": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.0.tgz", + "integrity": "sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.972.0.tgz", + "integrity": "sha512-AsmnNkW+RF+UQ86bYduMN4e6DuEAsy9nragtBAdGnlVYILTB7C2AjMVzp2EM1WOzjZ4dDsOUS/t099rzi+GcfQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@aws-sdk/util-format-url": "3.972.0", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.972.0.tgz", + "integrity": "sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.972.0.tgz", + "integrity": "sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.972.0", + "@aws-sdk/nested-clients": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.972.0.tgz", + "integrity": "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.0.tgz", + "integrity": "sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.972.0.tgz", + "integrity": "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.0.tgz", + "integrity": "sha512-o4zqsW/PxrcsTla/Yh2dkRS26kP76QQWZq/i/JgVNFBAr9x0E2oJcCeh8Daj2AA+8AZ8VWln9x706FFzWWQwvQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.3.tgz", + "integrity": "sha512-FNUqAjlKAGA7GM05kywE99q8wiPHPZqrzhq3wXRga6PRD6A0kzT85Pb0AzYBVTBRpSrKyyr6M92Y6bnSBVp2BA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.0.tgz", + "integrity": "sha512-eOLdkQyoRbDgioTS3Orr7iVsVEutJyMZxvyZ6WAF95IrF0kfWx5Rd/KXnfbnG/VKa2CvjZiitWfouLzfVEyvJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.972.0", + "@smithy/types": "^4.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.0.tgz", + "integrity": "sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.972.0", + "@aws-sdk/types": "3.972.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.0.tgz", + "integrity": "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1887,6 +2821,738 @@ "dev": true, "license": "MIT" }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", + "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", + "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.21.0.tgz", + "integrity": "sha512-bg2TfzgsERyETAxc/Ims/eJX8eAnIeTi4r4LHpMpfF/2NyO6RsWis0rjKcCPaGksljmOb23BZRiCeT/3NvwkXw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", + "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", + "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", + "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", + "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", + "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", + "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", + "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", + "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", + "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", + "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", + "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", + "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", + "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.10.tgz", + "integrity": "sha512-kwWpNltpxrvPabnjEFvwSmA+66l6s2ReCvgVSzW/z92LU4T28fTdgZ18IdYRYOrisu2NMQ0jUndRScbO65A/zg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.21.0", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-middleware": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.26.tgz", + "integrity": "sha512-ozZMoTAr+B2aVYfLYfkssFvc8ZV3p/vLpVQ7/k277xxUOA9ykSPe5obL2j6yHfbdrM/SZV7qj0uk/hSqavHrLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.10.11", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", + "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", + "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", + "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", + "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", + "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", + "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", + "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", + "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", + "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.10.11", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.11.tgz", + "integrity": "sha512-6o804SCyHGMXAb5mFJ+iTy9kVKv7F91a9szN0J+9X6p8A0NrdpUxdaC57aye2ipQkP2C4IAqETEpGZ0Zj77Haw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.21.0", + "@smithy/middleware-endpoint": "^4.4.10", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", + "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", + "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.25.tgz", + "integrity": "sha512-8ugoNMtss2dJHsXnqsibGPqoaafvWJPACmYKxJ4E6QWaDrixsAemmiMMAVbvwYadjR0H9G2+AlzsInSzRi8PSw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.10.11", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.28", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.28.tgz", + "integrity": "sha512-mjUdcP8h3E0K/XvNMi9oBXRV3DMCzeRiYIieZ1LQ7jq5tu6GH/GTWym7a1xIIE0pKSoLcpGsaImuQhGPSIJzAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.10.11", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", + "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", + "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", + "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", + "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", + "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", @@ -3125,6 +4791,12 @@ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4936,6 +6608,24 @@ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", "license": "Unlicense" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -8716,6 +10406,18 @@ } } }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", diff --git a/package.json b/package.json index a4b0c9d..17b76e2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ }, "dependencies": { "@auth/prisma-adapter": "^2.11.1", + "@aws-sdk/client-s3": "^3.972.0", + "@aws-sdk/s3-request-presigner": "^3.972.0", "@edge-runtime/cookies": "^6.0.0", "@prisma/client": "^5.7.0", "@stripe/stripe-js": "^8.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9dd25a9..b60cdaa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,168 +1,168 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "debian-openssl-3.0.x"] -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id @default(cuid()) - email String @unique - name String? - password String? - image String? - emailVerified DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Stripe subscription fields - stripeCustomerId String? @unique - stripeSubscriptionId String? @unique - stripePriceId String? - stripeCurrentPeriodEnd DateTime? - plan Plan @default(FREE) - - // Password reset fields - resetPasswordToken String? @unique - resetPasswordExpires DateTime? - - qrCodes QRCode[] - integrations Integration[] - accounts Account[] - sessions Session[] -} - -enum Plan { - FREE - PRO - BUSINESS -} - -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} - -model QRCode { - id String @id @default(cuid()) - userId String - title String - type QRType @default(DYNAMIC) - contentType ContentType @default(URL) - content Json - tags String[] - status QRStatus @default(ACTIVE) - style Json - slug String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - scans QRScan[] - - @@index([userId, createdAt]) -} - -enum QRType { - STATIC - DYNAMIC -} - -enum ContentType { - URL - VCARD - GEO - PHONE - SMS - TEXT - WHATSAPP - PDF - APP - COUPON - FEEDBACK -} - -enum QRStatus { - ACTIVE - PAUSED -} - -model QRScan { - id String @id @default(cuid()) - qrId String - ts DateTime @default(now()) - ipHash String - userAgent String? - device String? - os String? - country String? - referrer String? - utmSource String? - utmMedium String? - utmCampaign String? - isUnique Boolean @default(false) - - qr QRCode @relation(fields: [qrId], references: [id], onDelete: Cascade) - - @@index([qrId, ts]) -} - -model Integration { - id String @id @default(cuid()) - userId String - provider String - status String @default("inactive") - config Json - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -model NewsletterSubscription { - id String @id @default(cuid()) - email String @unique - source String @default("ai-coming-soon") - status String @default("subscribed") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([email]) - @@index([createdAt]) +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-3.0.x"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + email String @unique + name String? + password String? + image String? + emailVerified DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Stripe subscription fields + stripeCustomerId String? @unique + stripeSubscriptionId String? @unique + stripePriceId String? + stripeCurrentPeriodEnd DateTime? + plan Plan @default(FREE) + + // Password reset fields + resetPasswordToken String? @unique + resetPasswordExpires DateTime? + + qrCodes QRCode[] + integrations Integration[] + accounts Account[] + sessions Session[] +} + +enum Plan { + FREE + PRO + BUSINESS +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model QRCode { + id String @id @default(cuid()) + userId String + title String + type QRType @default(DYNAMIC) + contentType ContentType @default(URL) + content Json + tags String[] + status QRStatus @default(ACTIVE) + style Json + slug String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + scans QRScan[] + + @@index([userId, createdAt]) +} + +enum QRType { + STATIC + DYNAMIC +} + +enum ContentType { + URL + VCARD + GEO + PHONE + SMS + TEXT + WHATSAPP + PDF + APP + COUPON + FEEDBACK +} + +enum QRStatus { + ACTIVE + PAUSED +} + +model QRScan { + id String @id @default(cuid()) + qrId String + ts DateTime @default(now()) + ipHash String + userAgent String? + device String? + os String? + country String? + referrer String? + utmSource String? + utmMedium String? + utmCampaign String? + isUnique Boolean @default(false) + + qr QRCode @relation(fields: [qrId], references: [id], onDelete: Cascade) + + @@index([qrId, ts]) +} + +model Integration { + id String @id @default(cuid()) + userId String + provider String + status String @default("inactive") + config Json + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model NewsletterSubscription { + id String @id @default(cuid()) + email String @unique + source String @default("ai-coming-soon") + status String @default("subscribed") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([email]) + @@index([createdAt]) } \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml index d3ac98c..024f89c 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,33 +1,33 @@ - - - - https://www.qrmaster.net/ - 2025-10-16T00:00:00Z - daily - 0.9 - - - https://www.qrmaster.net/blog - 2025-10-16T00:00:00Z - daily - 0.7 - - - https://www.qrmaster.net/pricing - 2025-10-16T00:00:00Z - weekly - 0.8 - - - https://www.qrmaster.net/faq - 2025-10-16T00:00:00Z - weekly - 0.6 - - - https://www.qrmaster.net/blog/qr-code-analytics - 2025-10-16T00:00:00Z - weekly - 0.6 - - + + + + https://www.qrmaster.net/ + 2025-10-16T00:00:00Z + daily + 0.9 + + + https://www.qrmaster.net/blog + 2025-10-16T00:00:00Z + daily + 0.7 + + + https://www.qrmaster.net/pricing + 2025-10-16T00:00:00Z + weekly + 0.8 + + + https://www.qrmaster.net/faq + 2025-10-16T00:00:00Z + weekly + 0.6 + + + https://www.qrmaster.net/blog/qr-code-analytics + 2025-10-16T00:00:00Z + weekly + 0.6 + + diff --git a/seo_2026_jan.md b/seo_2026_jan.md new file mode 100644 index 0000000..5ae7587 --- /dev/null +++ b/seo_2026_jan.md @@ -0,0 +1,156 @@ +SEO Opportunity Report & Implementation Plan (Jan 2026) +1. Executive Summary +An analysis of the provided Google Keyword Planner data (Jan 22, 2026) reveals significant low-competition, high-volume traffic opportunities that were previously untapped. We have immediately capitalized on the Barcode opportunity and have a clear path to capture Custom QR intent. + +2. Key Data Findings ("Hidden Gems") +We identified three specific clusters where search volume is high but competition is exceptionally low. + +A. The "QR Barcode" Anomaly (Gold Mine) 🏆 +Users are confused about the terminology, searching for "qr barcode" or "bar code generator" instead of just "barcode". + +Keywords: qr barcode, bar code generator, scan code generator +Volume: 10k – 100k (High) +Competition: Low / Medium +Opportunity: Most competitors optimize for "Barcode Generator". By targeting the "wrong" terms users actually type, we can win easy traffic. +B. The "Free" Intent +High volume, but users are specifically looking for "free" and "no signup". + +Keyword: free qr code generator (100k – 1M) +Keyword: qr code generator free (100k – 1M) +Opportunity: Aggressive targeting of these exact match phrases on the homepage metadata. +C. The "Custom" Gap +Users want customization but don't always use the term "design". + +Keyword: custom qr code generator +Volume: 1k – 10k +Competition: Low +Current Status: MISSING. We do not have a dedicated landing page for this high-intent cluster. +3. Actions Already Implemented ✅ +We have immediately updated the metadata to capture the traffic identified in findings A and B. + +1. Barcode Generator Optimization +File: +src/app/(marketing)/tools/barcode-generator/page.tsx + +Action: Updated and meta description. +New Target: "QR Barcode" and "Bar Code Generator". +Why: To capture the 100k+ users searching for these specific variants. +2. Homepage Optimization +File: +src/app/(marketing)/page.tsx + +Action: Injected high-volume keyword tags. +New Target: qr generator, free qr code generator, custom qr code generator. +Why: To signal relevance to Google for the broadest "head terms". +4. Implementation Plan: "Custom QR Code" Landing Page 🚀 +To capture the 1k–10k/month users searching for "custom qr code generator" (Finding C), we need a dedicated landing page. This page will focus on design features (colors, logos, frames) rather than just "generating" a code. + +Phase 1: Page Structure (New File) +Path: src/app/(marketing)/tools/custom-qr-code-generator/page.tsx + +Content Strategy: + +H1: "Free Custom QR Code Generator with Logo & Colors" +Hero: Visual emphasis on beautiful codes, not black-and-white ones. +Live Editor Demo: (Reuse existing QRCodeGeneratorClient but pre-opened "Design" tab). +Features Section: +"Add Your Logo" +"Custom Colors & Gradients" +"Unique Frames & Shapes" +FAQ: Targeting specific "custom" questions (vector formats, scanning reliability). +Phase 2: Internal Linking +Add link to /tools/custom-qr-code-generator in the Footer and Main Navigation under "Tools". +Link from Homepage "Features" section. +Phase 3: Metadata Strategy +title: 'Custom QR Code Generator – Add Logo, Colors & Frames', +description: 'Create unique custom QR codes with your logo, colors, and frames. Free online customizer with high-quality vector download (SVG/PNG).', +keywords: ['custom qr code generator', 'qr code with logo', 'qr code design', 'creative qr code', 'branded qr code'] +5. Next Steps +Approval: Confirm this plan to build the custom-qr-code-generator page. +Execution: I will create the directory and page file. +Verify: Check that the new page renders and is linked correctly. + + + + + + + + + + + + +new findings + +Das ist super, dass du die Dateien hochgeladen hast! Denn: Du hast die Daten bereits. + +Ich habe deine Excel-Listen analysiert und das Problem gefunden: In deiner Ansicht im Tool hieß die Spalte vielleicht anders oder war versteckt, aber in der Datei heißt sie Avg. monthly searches. Das ist exakt das Suchvolumen, das du gesucht hast. + +Hier sind die echten "SchĂ€tze" direkt aus deiner Datei, gefiltert nach meiner Strategie: + +1. Die "Low Hanging Fruits" (Viel Traffic, fast 0 Konkurrenz) +Das sind die Keywords, bei denen du SOFORT ranken kannst, weil die großen Player schlafen. + +call qr code + +Suchvolumen: 50.000 (!) + +Wettbewerb-Index: 0 (Das ist extrem selten! Absolut gar keine Konkurrenz auf Google Ads). + +Idee: Mach eine Seite "Telefon-QR-Code erstellen". Leute wollen, dass beim Scannen direkt eine Nummer gewĂ€hlt wird. Das ist eine riesige Nische. + +generate website qr code + +Suchvolumen: 50.000 + +Wettbewerb-Index: 17 (Sehr gering). + +CPC: 6,16 € (Das ist der Jackpot: Viel Traffic, wenig Konkurrenz UND hoher Wert). + +bar code generator + +Suchvolumen: 500.000 + +Wettbewerb-Index: 3 (Fast nichts). + +BestĂ€tigt meine frĂŒhere Analyse: Das ist dein Traffic-Bringer. + +2. Die "Goldminen" (Hoher Klickpreis / High Ticket) +Hier zahlen Firmen richtig Geld. Wenn du hier rankst, sind die Nutzer sehr wertvoll. + +manage qr codes + +CPC (Gebot): 18,77 € + +Das suchen Firmen, die viele Codes verwalten mĂŒssen (Agenturen, Großkunden). Perfekt fĂŒr dein "Pro"-Abo. + +dynamic code generator + +CPC: 15,62 € + +Das Kern-Keyword fĂŒr dein Business-Modell. + +qr tracking + +CPC: 12,51 € + +Wettbewerb ist hier höher, aber der Umsatz pro Kunde auch. + +3. Der Trend-Tipp (+900% Wachstum) +free bulk qr code generator (Massen-Erstellung) + +Trend: +900% + +Viele Leute suchen nach einer Möglichkeit, 100 Codes auf einmal zu erstellen (z.B. fĂŒr Mitarbeiterausweise oder Inventar). Wenn du so ein Feature ("Bulk Upload") hast oder baust, bist du MarktfĂŒhrer. + +Was du jetzt tun musst (in Excel oder Google Sheets): +Du brauchst das Tool gar nicht mehr zwingend. Du kannst einfach in deiner Datei filtern: + +Such die Spalte Avg. monthly searches -> Das ist dein Suchvolumen. + +Such die Spalte Competition (indexed value) -> Das ist die HĂ€rte (0-100). + +Filter: Avg. monthly searches > 1000 UND Competition (indexed value) < 20. + +Damit findest du genau die oben genannten Chancen. Soll ich dir fĂŒr eines dieser Keywords (z.B. den "Call QR Code" mit Index 0) eine Gliederung schreiben? \ No newline at end of file diff --git a/src/app/(app)/create/page.tsx b/src/app/(app)/create/page.tsx index 4414184..96ccf6f 100644 --- a/src/app/(app)/create/page.tsx +++ b/src/app/(app)/create/page.tsx @@ -433,26 +433,95 @@ export default function CreatePage() { </div> ); case 'PDF': + const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { + const file = e.target.files?.[0]; + if (!file) return; + + // 10MB limit + if (file.size > 10 * 1024 * 1024) { + showToast('File size too large (max 10MB)', 'error'); + return; + } + + setUploading(true); + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }); + const data = await response.json(); + + if (response.ok) { + setContent({ ...content, fileUrl: data.url, fileName: data.filename }); + showToast('File uploaded successfully!', 'success'); + } else { + showToast(data.error || 'Upload failed', 'error'); + } + } catch (error) { + console.error('Upload error:', error); + showToast('Error uploading file', 'error'); + } finally { + setUploading(false); + } + }; + return ( <> <div> <div className="flex items-center mb-1"> - <label className="block text-sm font-medium text-gray-700">PDF/File URL</label> - <Tooltip text="Paste a public link to your PDF (Google Drive, Dropbox, etc.)" /> + <label className="block text-sm font-medium text-gray-700">Upload Menu / PDF</label> + <Tooltip text="Upload your menu PDF (Max 10MB). Hosted securely." /> + </div> + + <div className="mt-2 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg hover:bg-gray-50 transition-colors relative"> + <div className="space-y-1 text-center"> + {uploading ? ( + <div className="flex flex-col items-center"> + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500 mb-2"></div> + <p className="text-sm text-gray-500">Uploading...</p> + </div> + ) : content.fileUrl ? ( + <div className="flex flex-col items-center"> + <div className="mx-auto h-12 w-12 text-primary-500 bg-primary-50 rounded-full flex items-center justify-center mb-2"> + <FileText className="h-6 w-6" /> + </div> + <p className="text-sm text-green-600 font-medium mb-1">Upload Complete!</p> + <a href={content.fileUrl} target="_blank" rel="noopener noreferrer" className="text-xs text-primary-500 hover:underline break-all max-w-xs mb-3 block"> + {content.fileName || 'View File'} + </a> + <label htmlFor="file-upload" className="cursor-pointer bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"> + <span>Replace File</span> + <input id="file-upload" name="file-upload" type="file" className="sr-only" accept=".pdf,image/*" onChange={handleFileUpload} /> + </label> + </div> + ) : ( + <> + <Upload className="mx-auto h-12 w-12 text-gray-400" /> + <div className="flex text-sm text-gray-600 justify-center"> + <label htmlFor="file-upload" className="relative cursor-pointer bg-white rounded-md font-medium text-primary-600 hover:text-primary-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-primary-500"> + <span>Upload a file</span> + <input id="file-upload" name="file-upload" type="file" className="sr-only" accept=".pdf,image/*" onChange={handleFileUpload} /> + </label> + <p className="pl-1">or drag and drop</p> + </div> + <p className="text-xs text-gray-500">PDF, PNG, JPG up to 10MB</p> + </> + )} + </div> </div> - <Input - value={content.fileUrl || ''} - onChange={(e) => setContent({ ...content, fileUrl: e.target.value })} - placeholder="https://drive.google.com/file/d/.../view" - required - /> </div> - <Input - label="File Name (optional)" - value={content.fileName || ''} - onChange={(e) => setContent({ ...content, fileName: e.target.value })} - placeholder="Product Catalog 2026" - /> + + {content.fileUrl && ( + <Input + label="File Name / Menu Title" + value={content.fileName || ''} + onChange={(e) => setContent({ ...content, fileName: e.target.value })} + placeholder="Product Catalog 2026" + /> + )} </> ); case 'APP': diff --git a/src/app/(app)/pricing/page.tsx b/src/app/(app)/pricing/page.tsx index 037a3bc..4654ea7 100644 --- a/src/app/(app)/pricing/page.tsx +++ b/src/app/(app)/pricing/page.tsx @@ -1,268 +1,268 @@ -'use client'; - -import React, { useState, useEffect } from 'react'; -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'; -import { BillingToggle } from '@/components/ui/BillingToggle'; - -export default function PricingPage() { - const router = useRouter(); - const [loading, setLoading] = useState<string | null>(null); - const [currentPlan, setCurrentPlan] = useState<string>('FREE'); - const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null); - const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); - - useEffect(() => { - // 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'); - setCurrentInterval(data.interval || null); - } - } catch (error) { - console.error('Error fetching user plan:', error); - } - }; - - fetchUserPlan(); - }, []); - - const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => { - setLoading(plan); - - try { - const response = await fetch('/api/stripe/create-checkout-session', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - plan, - billingInterval: billingPeriod === 'month' ? 'month' : 'year', - }), - }); - - 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('Failed to start checkout. Please try again.', 'error'); - setLoading(null); - } - }; - - 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.' - ); - - if (!confirmed) { - return; - } - - setLoading('FREE'); - - try { - const response = await fetch('/api/stripe/cancel-subscription', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Failed to cancel subscription'); - } - - 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); - } - }; - - // Helper function to check if this is the user's exact current plan (plan + interval) - const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => { - return currentPlan === planType && currentInterval === interval; - }; - - // Helper function to check if user has this plan but different interval - const hasPlanDifferentInterval = (planType: string) => { - return currentPlan === planType && currentInterval && currentInterval !== billingPeriod; - }; - - const selectedInterval = billingPeriod === 'month' ? 'month' : 'year'; - - const plans = [ - { - key: 'free', - name: 'Free', - price: '€0', - period: 'forever', - showDiscount: false, - features: [ - '3 dynamic QR codes', - 'Unlimited static QR codes', - 'Basic scan tracking', - 'Standard QR design templates', - 'Download as SVG/PNG', - ], - buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free', - buttonVariant: 'outline' as const, - disabled: currentPlan === 'FREE', - popular: false, - onDowngrade: handleDowngrade, - }, - { - key: 'pro', - name: 'Pro', - price: billingPeriod === 'month' ? '€9' : '€90', - period: billingPeriod === 'month' ? 'per month' : 'per year', - showDiscount: billingPeriod === 'year', - features: [ - '50 dynamic QR codes', - 'Unlimited static QR codes', - 'Advanced analytics (scans, devices, locations)', - 'Custom branding (colors & logos)', - ], - buttonText: isCurrentPlanWithInterval('PRO', selectedInterval) - ? 'Current Plan' - : hasPlanDifferentInterval('PRO') - ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` - : 'Upgrade to Pro', - buttonVariant: 'primary' as const, - disabled: isCurrentPlanWithInterval('PRO', selectedInterval), - popular: true, - onUpgrade: () => handleUpgrade('PRO'), - }, - { - key: 'business', - name: 'Business', - price: billingPeriod === 'month' ? '€29' : '€290', - period: billingPeriod === 'month' ? 'per month' : 'per year', - showDiscount: billingPeriod === 'year', - 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: isCurrentPlanWithInterval('BUSINESS', selectedInterval) - ? 'Current Plan' - : hasPlanDifferentInterval('BUSINESS') - ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` - : 'Upgrade to Business', - buttonVariant: 'primary' as const, - disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval), - popular: false, - onUpgrade: () => handleUpgrade('BUSINESS'), - }, - ]; - - return ( - <div className="container mx-auto px-4 py-12"> - <div className="text-center mb-12"> - <h1 className="text-4xl font-bold text-gray-900 mb-4"> - Choose Your Plan - </h1> - <p className="text-xl text-gray-600"> - Select the perfect plan for your QR code needs - </p> - </div> - - <div className="flex justify-center mb-8"> - <BillingToggle value={billingPeriod} onChange={setBillingPeriod} /> - </div> - - <div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto"> - {plans.map((plan) => ( - <Card - key={plan.key} - className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''} - > - {plan.popular && ( - <div className="absolute -top-4 left-1/2 transform -translate-x-1/2"> - <Badge variant="info" className="px-3 py-1"> - Most Popular - </Badge> - </div> - )} - - <CardHeader className="text-center pb-8"> - <CardTitle className="text-2xl mb-4"> - {plan.name} - </CardTitle> - <div className="flex flex-col items-center"> - <div className="flex items-baseline justify-center"> - <span className="text-4xl font-bold"> - {plan.price} - </span> - <span className="text-gray-600 ml-2"> - {plan.period} - </span> - </div> - {plan.showDiscount && ( - <Badge variant="success" className="mt-2"> - Save 16% - </Badge> - )} - </div> - </CardHeader> - - <CardContent className="space-y-6"> - <ul className="space-y-3"> - {plan.features.map((feature: string, index: number) => ( - <li key={index} className="flex items-start space-x-3"> - <svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - <span className="text-gray-700">{feature}</span> - </li> - ))} - </ul> - - <Button - variant={plan.buttonVariant} - className="w-full" - size="lg" - disabled={plan.disabled || loading === plan.key.toUpperCase()} - onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade} - > - {loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText} - </Button> - </CardContent> - </Card> - ))} - </div> - - <div className="text-center mt-12"> - <p className="text-gray-600"> - All plans include unlimited static QR codes and basic customization. - </p> - <p className="text-gray-600 mt-2"> - Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a> - </p> - </div> - </div> - ); -} +'use client'; + +import React, { useState, useEffect } from 'react'; +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'; +import { BillingToggle } from '@/components/ui/BillingToggle'; + +export default function PricingPage() { + const router = useRouter(); + const [loading, setLoading] = useState<string | null>(null); + const [currentPlan, setCurrentPlan] = useState<string>('FREE'); + const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null); + const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); + + useEffect(() => { + // 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'); + setCurrentInterval(data.interval || null); + } + } catch (error) { + console.error('Error fetching user plan:', error); + } + }; + + fetchUserPlan(); + }, []); + + const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => { + setLoading(plan); + + try { + const response = await fetch('/api/stripe/create-checkout-session', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + plan, + billingInterval: billingPeriod === 'month' ? 'month' : 'year', + }), + }); + + 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('Failed to start checkout. Please try again.', 'error'); + setLoading(null); + } + }; + + 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.' + ); + + if (!confirmed) { + return; + } + + setLoading('FREE'); + + try { + const response = await fetch('/api/stripe/cancel-subscription', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to cancel subscription'); + } + + 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); + } + }; + + // Helper function to check if this is the user's exact current plan (plan + interval) + const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => { + return currentPlan === planType && currentInterval === interval; + }; + + // Helper function to check if user has this plan but different interval + const hasPlanDifferentInterval = (planType: string) => { + return currentPlan === planType && currentInterval && currentInterval !== billingPeriod; + }; + + const selectedInterval = billingPeriod === 'month' ? 'month' : 'year'; + + const plans = [ + { + key: 'free', + name: 'Free', + price: '€0', + period: 'forever', + showDiscount: false, + features: [ + '3 dynamic QR codes', + 'Unlimited static QR codes', + 'Basic scan tracking', + 'Standard QR design templates', + 'Download as SVG/PNG', + ], + buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free', + buttonVariant: 'outline' as const, + disabled: currentPlan === 'FREE', + popular: false, + onDowngrade: handleDowngrade, + }, + { + key: 'pro', + name: 'Pro', + price: billingPeriod === 'month' ? '€9' : '€90', + period: billingPeriod === 'month' ? 'per month' : 'per year', + showDiscount: billingPeriod === 'year', + features: [ + '50 dynamic QR codes', + 'Unlimited static QR codes', + 'Advanced analytics (scans, devices, locations)', + 'Custom branding (colors & logos)', + ], + buttonText: isCurrentPlanWithInterval('PRO', selectedInterval) + ? 'Current Plan' + : hasPlanDifferentInterval('PRO') + ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` + : 'Upgrade to Pro', + buttonVariant: 'primary' as const, + disabled: isCurrentPlanWithInterval('PRO', selectedInterval), + popular: true, + onUpgrade: () => handleUpgrade('PRO'), + }, + { + key: 'business', + name: 'Business', + price: billingPeriod === 'month' ? '€29' : '€290', + period: billingPeriod === 'month' ? 'per month' : 'per year', + showDiscount: billingPeriod === 'year', + 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: isCurrentPlanWithInterval('BUSINESS', selectedInterval) + ? 'Current Plan' + : hasPlanDifferentInterval('BUSINESS') + ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` + : 'Upgrade to Business', + buttonVariant: 'primary' as const, + disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval), + popular: false, + onUpgrade: () => handleUpgrade('BUSINESS'), + }, + ]; + + return ( + <div className="container mx-auto px-4 py-12"> + <div className="text-center mb-12"> + <h1 className="text-4xl font-bold text-gray-900 mb-4"> + Choose Your Plan + </h1> + <p className="text-xl text-gray-600"> + Select the perfect plan for your QR code needs + </p> + </div> + + <div className="flex justify-center mb-8"> + <BillingToggle value={billingPeriod} onChange={setBillingPeriod} /> + </div> + + <div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto"> + {plans.map((plan) => ( + <Card + key={plan.key} + className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''} + > + {plan.popular && ( + <div className="absolute -top-4 left-1/2 transform -translate-x-1/2"> + <Badge variant="info" className="px-3 py-1"> + Most Popular + </Badge> + </div> + )} + + <CardHeader className="text-center pb-8"> + <CardTitle className="text-2xl mb-4"> + {plan.name} + </CardTitle> + <div className="flex flex-col items-center"> + <div className="flex items-baseline justify-center"> + <span className="text-4xl font-bold"> + {plan.price} + </span> + <span className="text-gray-600 ml-2"> + {plan.period} + </span> + </div> + {plan.showDiscount && ( + <Badge variant="success" className="mt-2"> + Save 16% + </Badge> + )} + </div> + </CardHeader> + + <CardContent className="space-y-6"> + <ul className="space-y-3"> + {plan.features.map((feature: string, index: number) => ( + <li key={index} className="flex items-start space-x-3"> + <svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + <span className="text-gray-700">{feature}</span> + </li> + ))} + </ul> + + <Button + variant={plan.buttonVariant} + className="w-full" + size="lg" + disabled={plan.disabled || loading === plan.key.toUpperCase()} + onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade} + > + {loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText} + </Button> + </CardContent> + </Card> + ))} + </div> + + <div className="text-center mt-12"> + <p className="text-gray-600"> + All plans include unlimited static QR codes and basic customization. + </p> + <p className="text-gray-600 mt-2"> + Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a> + </p> + </div> + </div> + ); +} diff --git a/src/app/(app)/qr/[id]/edit/page.tsx b/src/app/(app)/qr/[id]/edit/page.tsx index fec5973..48e6806 100644 --- a/src/app/(app)/qr/[id]/edit/page.tsx +++ b/src/app/(app)/qr/[id]/edit/page.tsx @@ -1,372 +1,372 @@ -'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'; -import { useCsrf } from '@/hooks/useCsrf'; - -export default function EditQRPage() { - const router = useRouter(); - const params = useParams(); - const qrId = params.id as string; - const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); - - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [qrCode, setQrCode] = useState<any>(null); - const [title, setTitle] = useState(''); - const [content, setContent] = useState<any>({}); - - 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 fetchWithCsrf(`/api/qrs/${qrId}`, { - method: 'PATCH', - 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 ( - <div className="flex items-center justify-center min-h-screen"> - <div className="text-center"> - <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4"></div> - <p className="text-gray-600">Loading QR code...</p> - </div> - </div> - ); - } - - if (!qrCode) { - return null; - } - - // Static QR codes cannot be edited - if (qrCode.type === 'STATIC') { - return ( - <div className="max-w-2xl mx-auto mt-12"> - <Card> - <CardContent className="p-12 text-center"> - <div className="w-20 h-20 bg-warning-100 rounded-full flex items-center justify-center mx-auto mb-6"> - <svg className="w-10 h-10 text-warning-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> - </svg> - </div> - <h2 className="text-2xl font-bold text-gray-900 mb-2">Static QR Code</h2> - <p className="text-gray-600 mb-8"> - Static QR codes cannot be edited because their content is embedded directly in the QR code image. - </p> - <Button onClick={() => router.push('/dashboard')}> - Back to Dashboard - </Button> - </CardContent> - </Card> - </div> - ); - } - - return ( - <div className="max-w-3xl mx-auto"> - <div className="mb-8"> - <h1 className="text-3xl font-bold text-gray-900">Edit QR Code</h1> - <p className="text-gray-600 mt-2">Update your dynamic QR code content</p> - </div> - - <Card> - <CardHeader> - <CardTitle>QR Code Details</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <Input - label="Title" - value={title} - onChange={(e) => setTitle(e.target.value)} - placeholder="Enter QR code title" - required - /> - - {qrCode.contentType === 'URL' && ( - <Input - label="URL" - type="url" - value={content.url || ''} - onChange={(e) => setContent({ ...content, url: e.target.value })} - placeholder="https://example.com" - required - /> - )} - - {qrCode.contentType === 'PHONE' && ( - <Input - label="Phone Number" - type="tel" - value={content.phone || ''} - onChange={(e) => setContent({ ...content, phone: e.target.value })} - placeholder="+1234567890" - required - /> - )} - - {qrCode.contentType === 'VCARD' && ( - <> - <Input - label="First Name" - value={content.firstName || ''} - onChange={(e) => setContent({ ...content, firstName: e.target.value })} - placeholder="John" - required - /> - <Input - label="Last Name" - value={content.lastName || ''} - onChange={(e) => setContent({ ...content, lastName: e.target.value })} - placeholder="Doe" - required - /> - <Input - label="Email" - type="email" - value={content.email || ''} - onChange={(e) => setContent({ ...content, email: e.target.value })} - placeholder="john@example.com" - /> - <Input - label="Phone" - value={content.phone || ''} - onChange={(e) => setContent({ ...content, phone: e.target.value })} - placeholder="+1234567890" - /> - <Input - label="Organization" - value={content.organization || ''} - onChange={(e) => setContent({ ...content, organization: e.target.value })} - placeholder="Company Name" - /> - <Input - label="Job Title" - value={content.title || ''} - onChange={(e) => setContent({ ...content, title: e.target.value })} - placeholder="CEO" - /> - </> - )} - - {qrCode.contentType === 'GEO' && ( - <> - <Input - label="Latitude" - type="number" - step="any" - value={content.latitude || ''} - onChange={(e) => setContent({ ...content, latitude: parseFloat(e.target.value) || 0 })} - placeholder="37.7749" - required - /> - <Input - label="Longitude" - type="number" - step="any" - value={content.longitude || ''} - onChange={(e) => setContent({ ...content, longitude: parseFloat(e.target.value) || 0 })} - placeholder="-122.4194" - required - /> - <Input - label="Location Label (Optional)" - value={content.label || ''} - onChange={(e) => setContent({ ...content, label: e.target.value })} - placeholder="Golden Gate Bridge" - /> - </> - )} - - {qrCode.contentType === 'TEXT' && ( - <div> - <label className="block text-sm font-medium text-gray-700 mb-2"> - Text Content - </label> - <textarea - value={content.text || ''} - onChange={(e) => setContent({ ...content, text: e.target.value })} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" - rows={4} - placeholder="Enter your text content" - required - /> - </div> - )} - - {qrCode.contentType === 'PDF' && ( - <> - <Input - label="PDF/File URL" - value={content.fileUrl || ''} - onChange={(e) => setContent({ ...content, fileUrl: e.target.value })} - placeholder="https://drive.google.com/file/d/.../view" - required - /> - <Input - label="File Name (optional)" - value={content.fileName || ''} - onChange={(e) => setContent({ ...content, fileName: e.target.value })} - placeholder="Product Catalog 2026" - /> - </> - )} - - {qrCode.contentType === 'APP' && ( - <> - <Input - label="iOS App Store URL" - value={content.iosUrl || ''} - onChange={(e) => setContent({ ...content, iosUrl: e.target.value })} - placeholder="https://apps.apple.com/app/..." - /> - <Input - label="Android Play Store URL" - value={content.androidUrl || ''} - onChange={(e) => setContent({ ...content, androidUrl: e.target.value })} - placeholder="https://play.google.com/store/apps/..." - /> - <Input - label="Fallback URL (Desktop)" - value={content.fallbackUrl || ''} - onChange={(e) => setContent({ ...content, fallbackUrl: e.target.value })} - placeholder="https://yourapp.com" - /> - </> - )} - - {qrCode.contentType === 'COUPON' && ( - <> - <Input - label="Coupon Code" - value={content.code || ''} - onChange={(e) => setContent({ ...content, code: e.target.value })} - placeholder="SUMMER20" - required - /> - <Input - label="Discount" - value={content.discount || ''} - onChange={(e) => setContent({ ...content, discount: e.target.value })} - placeholder="20% OFF" - required - /> - <Input - label="Title" - value={content.title || ''} - onChange={(e) => setContent({ ...content, title: e.target.value })} - placeholder="Summer Sale 2026" - /> - <Input - label="Description (optional)" - value={content.description || ''} - onChange={(e) => setContent({ ...content, description: e.target.value })} - placeholder="Valid on all products" - /> - <Input - label="Expiry Date (optional)" - type="date" - value={content.expiryDate || ''} - onChange={(e) => setContent({ ...content, expiryDate: e.target.value })} - /> - <Input - label="Redeem URL (optional)" - value={content.redeemUrl || ''} - onChange={(e) => setContent({ ...content, redeemUrl: e.target.value })} - placeholder="https://shop.example.com" - /> - </> - )} - - {qrCode.contentType === 'FEEDBACK' && ( - <> - <Input - label="Business Name" - value={content.businessName || ''} - onChange={(e) => setContent({ ...content, businessName: e.target.value })} - placeholder="Your Restaurant Name" - required - /> - <Input - label="Google Review URL (optional)" - value={content.googleReviewUrl || ''} - onChange={(e) => setContent({ ...content, googleReviewUrl: e.target.value })} - placeholder="https://search.google.com/local/writereview?placeid=..." - /> - <Input - label="Thank You Message" - value={content.thankYouMessage || ''} - onChange={(e) => setContent({ ...content, thankYouMessage: e.target.value })} - placeholder="Thanks for your feedback!" - /> - </> - )} - - <div className="flex justify-end space-x-4 pt-4"> - <Button - variant="outline" - onClick={() => router.push('/dashboard')} - > - Cancel - </Button> - <Button - onClick={handleSave} - loading={saving} - disabled={csrfLoading || saving} - > - {csrfLoading ? 'Loading...' : 'Save Changes'} - </Button> - </div> - </CardContent> - </Card> - </div> - ); -} +'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'; +import { useCsrf } from '@/hooks/useCsrf'; + +export default function EditQRPage() { + const router = useRouter(); + const params = useParams(); + const qrId = params.id as string; + const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [qrCode, setQrCode] = useState<any>(null); + const [title, setTitle] = useState(''); + const [content, setContent] = useState<any>({}); + + 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 fetchWithCsrf(`/api/qrs/${qrId}`, { + method: 'PATCH', + 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 ( + <div className="flex items-center justify-center min-h-screen"> + <div className="text-center"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4"></div> + <p className="text-gray-600">Loading QR code...</p> + </div> + </div> + ); + } + + if (!qrCode) { + return null; + } + + // Static QR codes cannot be edited + if (qrCode.type === 'STATIC') { + return ( + <div className="max-w-2xl mx-auto mt-12"> + <Card> + <CardContent className="p-12 text-center"> + <div className="w-20 h-20 bg-warning-100 rounded-full flex items-center justify-center mx-auto mb-6"> + <svg className="w-10 h-10 text-warning-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> + </svg> + </div> + <h2 className="text-2xl font-bold text-gray-900 mb-2">Static QR Code</h2> + <p className="text-gray-600 mb-8"> + Static QR codes cannot be edited because their content is embedded directly in the QR code image. + </p> + <Button onClick={() => router.push('/dashboard')}> + Back to Dashboard + </Button> + </CardContent> + </Card> + </div> + ); + } + + return ( + <div className="max-w-3xl mx-auto"> + <div className="mb-8"> + <h1 className="text-3xl font-bold text-gray-900">Edit QR Code</h1> + <p className="text-gray-600 mt-2">Update your dynamic QR code content</p> + </div> + + <Card> + <CardHeader> + <CardTitle>QR Code Details</CardTitle> + </CardHeader> + <CardContent className="space-y-6"> + <Input + label="Title" + value={title} + onChange={(e) => setTitle(e.target.value)} + placeholder="Enter QR code title" + required + /> + + {qrCode.contentType === 'URL' && ( + <Input + label="URL" + type="url" + value={content.url || ''} + onChange={(e) => setContent({ ...content, url: e.target.value })} + placeholder="https://example.com" + required + /> + )} + + {qrCode.contentType === 'PHONE' && ( + <Input + label="Phone Number" + type="tel" + value={content.phone || ''} + onChange={(e) => setContent({ ...content, phone: e.target.value })} + placeholder="+1234567890" + required + /> + )} + + {qrCode.contentType === 'VCARD' && ( + <> + <Input + label="First Name" + value={content.firstName || ''} + onChange={(e) => setContent({ ...content, firstName: e.target.value })} + placeholder="John" + required + /> + <Input + label="Last Name" + value={content.lastName || ''} + onChange={(e) => setContent({ ...content, lastName: e.target.value })} + placeholder="Doe" + required + /> + <Input + label="Email" + type="email" + value={content.email || ''} + onChange={(e) => setContent({ ...content, email: e.target.value })} + placeholder="john@example.com" + /> + <Input + label="Phone" + value={content.phone || ''} + onChange={(e) => setContent({ ...content, phone: e.target.value })} + placeholder="+1234567890" + /> + <Input + label="Organization" + value={content.organization || ''} + onChange={(e) => setContent({ ...content, organization: e.target.value })} + placeholder="Company Name" + /> + <Input + label="Job Title" + value={content.title || ''} + onChange={(e) => setContent({ ...content, title: e.target.value })} + placeholder="CEO" + /> + </> + )} + + {qrCode.contentType === 'GEO' && ( + <> + <Input + label="Latitude" + type="number" + step="any" + value={content.latitude || ''} + onChange={(e) => setContent({ ...content, latitude: parseFloat(e.target.value) || 0 })} + placeholder="37.7749" + required + /> + <Input + label="Longitude" + type="number" + step="any" + value={content.longitude || ''} + onChange={(e) => setContent({ ...content, longitude: parseFloat(e.target.value) || 0 })} + placeholder="-122.4194" + required + /> + <Input + label="Location Label (Optional)" + value={content.label || ''} + onChange={(e) => setContent({ ...content, label: e.target.value })} + placeholder="Golden Gate Bridge" + /> + </> + )} + + {qrCode.contentType === 'TEXT' && ( + <div> + <label className="block text-sm font-medium text-gray-700 mb-2"> + Text Content + </label> + <textarea + value={content.text || ''} + onChange={(e) => setContent({ ...content, text: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" + rows={4} + placeholder="Enter your text content" + required + /> + </div> + )} + + {qrCode.contentType === 'PDF' && ( + <> + <Input + label="PDF/File URL" + value={content.fileUrl || ''} + onChange={(e) => setContent({ ...content, fileUrl: e.target.value })} + placeholder="https://drive.google.com/file/d/.../view" + required + /> + <Input + label="File Name (optional)" + value={content.fileName || ''} + onChange={(e) => setContent({ ...content, fileName: e.target.value })} + placeholder="Product Catalog 2026" + /> + </> + )} + + {qrCode.contentType === 'APP' && ( + <> + <Input + label="iOS App Store URL" + value={content.iosUrl || ''} + onChange={(e) => setContent({ ...content, iosUrl: e.target.value })} + placeholder="https://apps.apple.com/app/..." + /> + <Input + label="Android Play Store URL" + value={content.androidUrl || ''} + onChange={(e) => setContent({ ...content, androidUrl: e.target.value })} + placeholder="https://play.google.com/store/apps/..." + /> + <Input + label="Fallback URL (Desktop)" + value={content.fallbackUrl || ''} + onChange={(e) => setContent({ ...content, fallbackUrl: e.target.value })} + placeholder="https://yourapp.com" + /> + </> + )} + + {qrCode.contentType === 'COUPON' && ( + <> + <Input + label="Coupon Code" + value={content.code || ''} + onChange={(e) => setContent({ ...content, code: e.target.value })} + placeholder="SUMMER20" + required + /> + <Input + label="Discount" + value={content.discount || ''} + onChange={(e) => setContent({ ...content, discount: e.target.value })} + placeholder="20% OFF" + required + /> + <Input + label="Title" + value={content.title || ''} + onChange={(e) => setContent({ ...content, title: e.target.value })} + placeholder="Summer Sale 2026" + /> + <Input + label="Description (optional)" + value={content.description || ''} + onChange={(e) => setContent({ ...content, description: e.target.value })} + placeholder="Valid on all products" + /> + <Input + label="Expiry Date (optional)" + type="date" + value={content.expiryDate || ''} + onChange={(e) => setContent({ ...content, expiryDate: e.target.value })} + /> + <Input + label="Redeem URL (optional)" + value={content.redeemUrl || ''} + onChange={(e) => setContent({ ...content, redeemUrl: e.target.value })} + placeholder="https://shop.example.com" + /> + </> + )} + + {qrCode.contentType === 'FEEDBACK' && ( + <> + <Input + label="Business Name" + value={content.businessName || ''} + onChange={(e) => setContent({ ...content, businessName: e.target.value })} + placeholder="Your Restaurant Name" + required + /> + <Input + label="Google Review URL (optional)" + value={content.googleReviewUrl || ''} + onChange={(e) => setContent({ ...content, googleReviewUrl: e.target.value })} + placeholder="https://search.google.com/local/writereview?placeid=..." + /> + <Input + label="Thank You Message" + value={content.thankYouMessage || ''} + onChange={(e) => setContent({ ...content, thankYouMessage: e.target.value })} + placeholder="Thanks for your feedback!" + /> + </> + )} + + <div className="flex justify-end space-x-4 pt-4"> + <Button + variant="outline" + onClick={() => router.push('/dashboard')} + > + Cancel + </Button> + <Button + onClick={handleSave} + loading={saving} + disabled={csrfLoading || saving} + > + {csrfLoading ? 'Loading...' : 'Save Changes'} + </Button> + </div> + </CardContent> + </Card> + </div> + ); +} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 63fde07..39de12d 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,11 +1,11 @@ -export default function AuthLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white"> - {children} - </div> - ); +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white"> + {children} + </div> + ); } \ No newline at end of file diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 440505a..cf1477d 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,187 +1,187 @@ -'use client'; - -import React, { useState, useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import Link from 'next/link'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { useTranslation } from '@/hooks/useTranslation'; -import { useCsrf } from '@/hooks/useCsrf'; - -export default function LoginPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const { t } = useTranslation(); - const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(''); - - try { - const response = await fetchWithCsrf('/api/auth/simple-login', { - method: 'POST', - body: JSON.stringify({ email, password }), - }); - - const data = await response.json(); - - if (response.ok && data.success) { - // Store user in localStorage for client-side - localStorage.setItem('user', JSON.stringify(data.user)); - - // Track successful login with PostHog - try { - const { identifyUser, trackEvent } = await import('@/components/PostHogProvider'); - identifyUser(data.user.id, { - email: data.user.email, - name: data.user.name, - plan: data.user.plan || 'FREE', - }); - trackEvent('user_login', { - method: 'email', - email: data.user.email, - }); - } catch (error) { - console.error('PostHog tracking error:', error); - } - - // Check for redirect parameter - const redirectUrl = searchParams.get('redirect') || '/dashboard'; - router.push(redirectUrl); - router.refresh(); - } else { - setError(data.error || 'Invalid email or password'); - } - } catch (err) { - setError('An error occurred. Please try again.'); - } finally { - setLoading(false); - } - }; - - const handleGoogleSignIn = () => { - // Redirect to Google OAuth API route - window.location.href = '/api/auth/google'; - }; - - return ( - <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> - <div className="w-full max-w-md"> - <div className="text-center mb-8"> - <Link href="/" className="inline-flex items-center space-x-2 mb-6"> - <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> - <span className="text-2xl font-bold text-gray-900">QR Master</span> - </Link> - <h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1> - <p className="text-gray-600 mt-2">Sign in to your account</p> - <Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors"> - ← Back to Home - </Link> - </div> - - <Card> - <CardContent className="p-6"> - <form onSubmit={handleSubmit} className="space-y-4"> - {error && ( - <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm"> - {error} - </div> - )} - - <Input - label="Email" - type="email" - value={email} - onChange={(e) => setEmail(e.target.value)} - placeholder="you@example.com" - required - /> - - <Input - label="Password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - placeholder="‱‱‱‱‱‱‱‱" - required - /> - - <div className="flex items-center justify-between"> - <label className="flex items-center"> - <input type="checkbox" className="mr-2" /> - <span className="text-sm text-gray-600">Remember me</span> - </label> - <Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700"> - Forgot password? - </Link> - </div> - - <Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}> - {csrfLoading ? 'Loading...' : 'Sign In'} - </Button> - - <div className="relative my-6"> - <div className="absolute inset-0 flex items-center"> - <div className="w-full border-t border-gray-300"></div> - </div> - <div className="relative flex justify-center text-sm"> - <span className="px-2 bg-white text-gray-500">Or continue with</span> - </div> - </div> - - <Button - type="button" - variant="outline" - className="w-full" - onClick={handleGoogleSignIn} - > - <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> - <path - fill="#4285F4" - d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" - /> - <path - fill="#34A853" - d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" - /> - <path - fill="#FBBC05" - d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" - /> - <path - fill="#EA4335" - d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" - /> - </svg> - Sign in with Google - </Button> - </form> - - <div className="mt-6 text-center"> - <p className="text-sm text-gray-600"> - Don't have an account?{' '} - <Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium"> - Sign up - </Link> - </p> - </div> - </CardContent> - </Card> - - <p className="text-center text-sm text-gray-500 mt-6"> - By signing in, you agree to our{' '} - <Link href="/privacy" className="text-primary-600 hover:text-primary-700"> - Privacy Policy - </Link> - </p> - </div> - </div> - ); +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Link from 'next/link'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useCsrf } from '@/hooks/useCsrf'; + +export default function LoginPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { t } = useTranslation(); + const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await fetchWithCsrf('/api/auth/simple-login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + // Store user in localStorage for client-side + localStorage.setItem('user', JSON.stringify(data.user)); + + // Track successful login with PostHog + try { + const { identifyUser, trackEvent } = await import('@/components/PostHogProvider'); + identifyUser(data.user.id, { + email: data.user.email, + name: data.user.name, + plan: data.user.plan || 'FREE', + }); + trackEvent('user_login', { + method: 'email', + email: data.user.email, + }); + } catch (error) { + console.error('PostHog tracking error:', error); + } + + // Check for redirect parameter + const redirectUrl = searchParams.get('redirect') || '/dashboard'; + router.push(redirectUrl); + router.refresh(); + } else { + setError(data.error || 'Invalid email or password'); + } + } catch (err) { + setError('An error occurred. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleGoogleSignIn = () => { + // Redirect to Google OAuth API route + window.location.href = '/api/auth/google'; + }; + + return ( + <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> + <div className="w-full max-w-md"> + <div className="text-center mb-8"> + <Link href="/" className="inline-flex items-center space-x-2 mb-6"> + <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> + <span className="text-2xl font-bold text-gray-900">QR Master</span> + </Link> + <h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1> + <p className="text-gray-600 mt-2">Sign in to your account</p> + <Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors"> + ← Back to Home + </Link> + </div> + + <Card> + <CardContent className="p-6"> + <form onSubmit={handleSubmit} className="space-y-4"> + {error && ( + <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm"> + {error} + </div> + )} + + <Input + label="Email" + type="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="you@example.com" + required + /> + + <Input + label="Password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="‱‱‱‱‱‱‱‱" + required + /> + + <div className="flex items-center justify-between"> + <label className="flex items-center"> + <input type="checkbox" className="mr-2" /> + <span className="text-sm text-gray-600">Remember me</span> + </label> + <Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700"> + Forgot password? + </Link> + </div> + + <Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}> + {csrfLoading ? 'Loading...' : 'Sign In'} + </Button> + + <div className="relative my-6"> + <div className="absolute inset-0 flex items-center"> + <div className="w-full border-t border-gray-300"></div> + </div> + <div className="relative flex justify-center text-sm"> + <span className="px-2 bg-white text-gray-500">Or continue with</span> + </div> + </div> + + <Button + type="button" + variant="outline" + className="w-full" + onClick={handleGoogleSignIn} + > + <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> + <path + fill="#4285F4" + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" + /> + <path + fill="#34A853" + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" + /> + <path + fill="#FBBC05" + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" + /> + <path + fill="#EA4335" + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" + /> + </svg> + Sign in with Google + </Button> + </form> + + <div className="mt-6 text-center"> + <p className="text-sm text-gray-600"> + Don't have an account?{' '} + <Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium"> + Sign up + </Link> + </p> + </div> + </CardContent> + </Card> + + <p className="text-center text-sm text-gray-500 mt-6"> + By signing in, you agree to our{' '} + <Link href="/privacy" className="text-primary-600 hover:text-primary-700"> + Privacy Policy + </Link> + </p> + </div> + </div> + ); } \ No newline at end of file diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx index 4be1774..909da5a 100644 --- a/src/app/(auth)/reset-password/page.tsx +++ b/src/app/(auth)/reset-password/page.tsx @@ -1,208 +1,208 @@ -'use client'; - -import React, { useState, useEffect } from 'react'; -import Link from 'next/link'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { Card, CardContent } from '@/components/ui/Card'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { useCsrf } from '@/hooks/useCsrf'; - -export default function ResetPasswordPage() { - const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); - const searchParams = useSearchParams(); - const router = useRouter(); - - const [token, setToken] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - const [error, setError] = useState(''); - - useEffect(() => { - const tokenParam = searchParams.get('token'); - if (!tokenParam) { - setError('Invalid or missing reset token. Please request a new password reset link.'); - } else { - setToken(tokenParam); - } - }, [searchParams]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(''); - - // Validate passwords match - if (password !== confirmPassword) { - setError('Passwords do not match'); - setLoading(false); - return; - } - - // Validate password length - if (password.length < 8) { - setError('Password must be at least 8 characters long'); - setLoading(false); - return; - } - - try { - const response = await fetchWithCsrf('/api/auth/reset-password', { - method: 'POST', - body: JSON.stringify({ token, password }), - }); - - const data = await response.json(); - - if (response.ok) { - setSuccess(true); - // Redirect to login after 3 seconds - setTimeout(() => { - router.push('/login'); - }, 3000); - } else { - setError(data.error || 'Failed to reset password'); - } - } catch (err) { - setError('An error occurred. Please try again.'); - } finally { - setLoading(false); - } - }; - - if (success) { - return ( - <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> - <div className="w-full max-w-md"> - <div className="text-center mb-8"> - <Link href="/" className="inline-flex items-center space-x-2 mb-6"> - <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> - <span className="text-2xl font-bold text-gray-900">QR Master</span> - </Link> - <h1 className="text-3xl font-bold text-gray-900">Password Reset Successful</h1> - <p className="text-gray-600 mt-2">Your password has been updated</p> - </div> - - <Card> - <CardContent className="p-6"> - <div className="text-center"> - <div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4"> - <svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> - </svg> - </div> - - <p className="text-gray-700 mb-4"> - Your password has been successfully reset! - </p> - - <p className="text-sm text-gray-600 mb-6"> - Redirecting you to the login page in 3 seconds... - </p> - - <Link href="/login" className="block"> - <Button variant="primary" className="w-full"> - Go to Login - </Button> - </Link> - </div> - </CardContent> - </Card> - </div> - </div> - ); - } - - return ( - <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> - <div className="w-full max-w-md"> - <div className="text-center mb-8"> - <Link href="/" className="inline-flex items-center space-x-2 mb-6"> - <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> - <span className="text-2xl font-bold text-gray-900">QR Master</span> - </Link> - <h1 className="text-3xl font-bold text-gray-900">Reset Your Password</h1> - <p className="text-gray-600 mt-2">Enter your new password below</p> - </div> - - <Card> - <CardContent className="p-6"> - {!token ? ( - <div className="text-center"> - <div className="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4"> - <svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> - </svg> - </div> - <p className="text-red-600 mb-4">{error}</p> - <Link href="/forgot-password" className="block"> - <Button variant="primary" className="w-full"> - Request New Reset Link - </Button> - </Link> - </div> - ) : ( - <form onSubmit={handleSubmit} className="space-y-4"> - {error && ( - <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm"> - {error} - </div> - )} - - <Input - label="New Password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - placeholder="Enter new password" - required - disabled={loading || csrfLoading} - minLength={8} - /> - - <Input - label="Confirm Password" - type="password" - value={confirmPassword} - onChange={(e) => setConfirmPassword(e.target.value)} - placeholder="Confirm new password" - required - disabled={loading || csrfLoading} - minLength={8} - /> - - <div className="text-xs text-gray-500"> - Password must be at least 8 characters long - </div> - - <Button - type="submit" - className="w-full" - loading={loading} - disabled={csrfLoading || loading} - > - {csrfLoading ? 'Loading...' : 'Reset Password'} - </Button> - - <div className="text-center"> - <Link href="/login" className="text-sm text-primary-600 hover:text-primary-700 font-medium"> - ← Back to Login - </Link> - </div> - </form> - )} - </CardContent> - </Card> - - <p className="text-center text-sm text-gray-500 mt-6"> - Remember your password?{' '} - <Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium"> - Sign in - </Link> - </p> - </div> - </div> - ); -} +'use client'; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { Card, CardContent } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; +import { useCsrf } from '@/hooks/useCsrf'; + +export default function ResetPasswordPage() { + const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); + const searchParams = useSearchParams(); + const router = useRouter(); + + const [token, setToken] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + const tokenParam = searchParams.get('token'); + if (!tokenParam) { + setError('Invalid or missing reset token. Please request a new password reset link.'); + } else { + setToken(tokenParam); + } + }, [searchParams]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + // Validate passwords match + if (password !== confirmPassword) { + setError('Passwords do not match'); + setLoading(false); + return; + } + + // Validate password length + if (password.length < 8) { + setError('Password must be at least 8 characters long'); + setLoading(false); + return; + } + + try { + const response = await fetchWithCsrf('/api/auth/reset-password', { + method: 'POST', + body: JSON.stringify({ token, password }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess(true); + // Redirect to login after 3 seconds + setTimeout(() => { + router.push('/login'); + }, 3000); + } else { + setError(data.error || 'Failed to reset password'); + } + } catch (err) { + setError('An error occurred. Please try again.'); + } finally { + setLoading(false); + } + }; + + if (success) { + return ( + <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> + <div className="w-full max-w-md"> + <div className="text-center mb-8"> + <Link href="/" className="inline-flex items-center space-x-2 mb-6"> + <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> + <span className="text-2xl font-bold text-gray-900">QR Master</span> + </Link> + <h1 className="text-3xl font-bold text-gray-900">Password Reset Successful</h1> + <p className="text-gray-600 mt-2">Your password has been updated</p> + </div> + + <Card> + <CardContent className="p-6"> + <div className="text-center"> + <div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4"> + <svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> + </svg> + </div> + + <p className="text-gray-700 mb-4"> + Your password has been successfully reset! + </p> + + <p className="text-sm text-gray-600 mb-6"> + Redirecting you to the login page in 3 seconds... + </p> + + <Link href="/login" className="block"> + <Button variant="primary" className="w-full"> + Go to Login + </Button> + </Link> + </div> + </CardContent> + </Card> + </div> + </div> + ); + } + + return ( + <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> + <div className="w-full max-w-md"> + <div className="text-center mb-8"> + <Link href="/" className="inline-flex items-center space-x-2 mb-6"> + <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> + <span className="text-2xl font-bold text-gray-900">QR Master</span> + </Link> + <h1 className="text-3xl font-bold text-gray-900">Reset Your Password</h1> + <p className="text-gray-600 mt-2">Enter your new password below</p> + </div> + + <Card> + <CardContent className="p-6"> + {!token ? ( + <div className="text-center"> + <div className="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4"> + <svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </div> + <p className="text-red-600 mb-4">{error}</p> + <Link href="/forgot-password" className="block"> + <Button variant="primary" className="w-full"> + Request New Reset Link + </Button> + </Link> + </div> + ) : ( + <form onSubmit={handleSubmit} className="space-y-4"> + {error && ( + <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm"> + {error} + </div> + )} + + <Input + label="New Password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="Enter new password" + required + disabled={loading || csrfLoading} + minLength={8} + /> + + <Input + label="Confirm Password" + type="password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + required + disabled={loading || csrfLoading} + minLength={8} + /> + + <div className="text-xs text-gray-500"> + Password must be at least 8 characters long + </div> + + <Button + type="submit" + className="w-full" + loading={loading} + disabled={csrfLoading || loading} + > + {csrfLoading ? 'Loading...' : 'Reset Password'} + </Button> + + <div className="text-center"> + <Link href="/login" className="text-sm text-primary-600 hover:text-primary-700 font-medium"> + ← Back to Login + </Link> + </div> + </form> + )} + </CardContent> + </Card> + + <p className="text-center text-sm text-gray-500 mt-6"> + Remember your password?{' '} + <Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium"> + Sign in + </Link> + </p> + </div> + </div> + ); +} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 4782134..c184077 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,208 +1,208 @@ -'use client'; - -import React, { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { useTranslation } from '@/hooks/useTranslation'; -import { useCsrf } from '@/hooks/useCsrf'; - -export default function SignupPage() { - const router = useRouter(); - const { t } = useTranslation(); - const { fetchWithCsrf } = useCsrf(); - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(''); - - if (password !== confirmPassword) { - setError('Passwords do not match'); - setLoading(false); - return; - } - - if (password.length < 8) { - setError('Password must be at least 8 characters'); - setLoading(false); - return; - } - - try { - const response = await fetchWithCsrf('/api/auth/signup', { - method: 'POST', - body: JSON.stringify({ name, email, password }), - }); - - const data = await response.json(); - - if (response.ok && data.success) { - // Store user in localStorage for client-side - localStorage.setItem('user', JSON.stringify(data.user)); - - // Track successful signup with PostHog - try { - const { identifyUser, trackEvent } = await import('@/components/PostHogProvider'); - identifyUser(data.user.id, { - email: data.user.email, - name: data.user.name, - plan: data.user.plan || 'FREE', - signupMethod: 'email', - }); - trackEvent('user_signup', { - method: 'email', - email: data.user.email, - }); - } catch (error) { - console.error('PostHog tracking error:', error); - } - - // Redirect to dashboard - router.push('/dashboard'); - router.refresh(); - } else { - setError(data.error || 'Failed to create account'); - } - } catch (err) { - setError('An error occurred. Please try again.'); - } finally { - setLoading(false); - } - }; - - const handleGoogleSignIn = () => { - // Redirect to Google OAuth API route - window.location.href = '/api/auth/google'; - }; - - return ( - <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> - <div className="w-full max-w-md"> - <div className="text-center mb-8"> - <Link href="/" className="inline-flex items-center space-x-2 mb-6"> - <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> - <span className="text-2xl font-bold text-gray-900">QR Master</span> - </Link> - <h1 className="text-3xl font-bold text-gray-900">Create Account</h1> - <p className="text-gray-600 mt-2">Start creating QR codes in seconds</p> - <Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors"> - ← Back to Home - </Link> - </div> - - <Card> - <CardContent className="p-6"> - <form onSubmit={handleSubmit} className="space-y-4"> - {error && ( - <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm"> - {error} - </div> - )} - - <Input - label="Full Name" - type="text" - value={name} - onChange={(e) => setName(e.target.value)} - placeholder="John Doe" - required - /> - - <Input - label="Email" - type="email" - value={email} - onChange={(e) => setEmail(e.target.value)} - placeholder="you@example.com" - required - /> - - <Input - label="Password" - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - placeholder="‱‱‱‱‱‱‱‱" - required - /> - - <Input - label="Confirm Password" - type="password" - value={confirmPassword} - onChange={(e) => setConfirmPassword(e.target.value)} - placeholder="‱‱‱‱‱‱‱‱" - required - /> - - <Button type="submit" className="w-full" loading={loading}> - Create Account - </Button> - - <div className="relative my-6"> - <div className="absolute inset-0 flex items-center"> - <div className="w-full border-t border-gray-300"></div> - </div> - <div className="relative flex justify-center text-sm"> - <span className="px-2 bg-white text-gray-500">Or continue with</span> - </div> - </div> - - <Button - type="button" - variant="outline" - className="w-full" - onClick={handleGoogleSignIn} - > - <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> - <path - fill="#4285F4" - d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" - /> - <path - fill="#34A853" - d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" - /> - <path - fill="#FBBC05" - d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" - /> - <path - fill="#EA4335" - d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" - /> - </svg> - Sign up with Google - </Button> - </form> - - <div className="mt-6 text-center"> - <p className="text-sm text-gray-600"> - Already have an account?{' '} - <Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium"> - Sign in - </Link> - </p> - </div> - </CardContent> - </Card> - - <p className="text-center text-sm text-gray-500 mt-6"> - By signing up, you agree to our{' '} - <Link href="/privacy" className="text-primary-600 hover:text-primary-700"> - Privacy Policy - </Link> - </p> - </div> - </div> - ); +'use client'; + +import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useCsrf } from '@/hooks/useCsrf'; + +export default function SignupPage() { + const router = useRouter(); + const { t } = useTranslation(); + const { fetchWithCsrf } = useCsrf(); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + if (password !== confirmPassword) { + setError('Passwords do not match'); + setLoading(false); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters'); + setLoading(false); + return; + } + + try { + const response = await fetchWithCsrf('/api/auth/signup', { + method: 'POST', + body: JSON.stringify({ name, email, password }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + // Store user in localStorage for client-side + localStorage.setItem('user', JSON.stringify(data.user)); + + // Track successful signup with PostHog + try { + const { identifyUser, trackEvent } = await import('@/components/PostHogProvider'); + identifyUser(data.user.id, { + email: data.user.email, + name: data.user.name, + plan: data.user.plan || 'FREE', + signupMethod: 'email', + }); + trackEvent('user_signup', { + method: 'email', + email: data.user.email, + }); + } catch (error) { + console.error('PostHog tracking error:', error); + } + + // Redirect to dashboard + router.push('/dashboard'); + router.refresh(); + } else { + setError(data.error || 'Failed to create account'); + } + } catch (err) { + setError('An error occurred. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleGoogleSignIn = () => { + // Redirect to Google OAuth API route + window.location.href = '/api/auth/google'; + }; + + return ( + <div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> + <div className="w-full max-w-md"> + <div className="text-center mb-8"> + <Link href="/" className="inline-flex items-center space-x-2 mb-6"> + <img src="/logo.svg" alt="QR Master" className="w-10 h-10" /> + <span className="text-2xl font-bold text-gray-900">QR Master</span> + </Link> + <h1 className="text-3xl font-bold text-gray-900">Create Account</h1> + <p className="text-gray-600 mt-2">Start creating QR codes in seconds</p> + <Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors"> + ← Back to Home + </Link> + </div> + + <Card> + <CardContent className="p-6"> + <form onSubmit={handleSubmit} className="space-y-4"> + {error && ( + <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm"> + {error} + </div> + )} + + <Input + label="Full Name" + type="text" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="John Doe" + required + /> + + <Input + label="Email" + type="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="you@example.com" + required + /> + + <Input + label="Password" + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="‱‱‱‱‱‱‱‱" + required + /> + + <Input + label="Confirm Password" + type="password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + placeholder="‱‱‱‱‱‱‱‱" + required + /> + + <Button type="submit" className="w-full" loading={loading}> + Create Account + </Button> + + <div className="relative my-6"> + <div className="absolute inset-0 flex items-center"> + <div className="w-full border-t border-gray-300"></div> + </div> + <div className="relative flex justify-center text-sm"> + <span className="px-2 bg-white text-gray-500">Or continue with</span> + </div> + </div> + + <Button + type="button" + variant="outline" + className="w-full" + onClick={handleGoogleSignIn} + > + <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24"> + <path + fill="#4285F4" + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" + /> + <path + fill="#34A853" + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" + /> + <path + fill="#FBBC05" + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" + /> + <path + fill="#EA4335" + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" + /> + </svg> + Sign up with Google + </Button> + </form> + + <div className="mt-6 text-center"> + <p className="text-sm text-gray-600"> + Already have an account?{' '} + <Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium"> + Sign in + </Link> + </p> + </div> + </CardContent> + </Card> + + <p className="text-center text-sm text-gray-500 mt-6"> + By signing up, you agree to our{' '} + <Link href="/privacy" className="text-primary-600 hover:text-primary-700"> + Privacy Policy + </Link> + </p> + </div> + </div> + ); } \ No newline at end of file diff --git a/src/app/(marketing)/blog/[slug]/page.tsx b/src/app/(marketing)/blog/[slug]/page.tsx index 094da3f..a84c8fc 100644 --- a/src/app/(marketing)/blog/[slug]/page.tsx +++ b/src/app/(marketing)/blog/[slug]/page.tsx @@ -1,2630 +1,2630 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import Link from 'next/link'; -import Image from 'next/image'; -import { notFound } from 'next/navigation'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; -import { blogPostingSchema, breadcrumbSchema, howToSchema } from '@/lib/schema'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; - -interface BlogPostData { - slug: string; - title: string; - excerpt: string; - date: string; - datePublished: string; - dateModified: string; - readTime: string; - category: string; - image: string; - imageAlt: string; - author: string; - authorUrl: string; - answer?: string; - howTo?: any; - content: string; -} - -const blogPosts: Record<string, BlogPostData> = { - 'qr-code-analytics': { - slug: 'qr-code-analytics', - title: 'QR Code Analytics: Track, Measure & Optimize', - excerpt: 'Master scan analytics, campaign tracking & dashboard insights to maximize QR ROI with dynamic codes.', - date: 'October 16, 2025', - datePublished: '2025-10-16T09:00:00Z', - dateModified: '2025-10-16T09:00:00Z', - readTime: '15 Min', - category: 'Analytics', - image: '/blog/4-hero.png', - imageAlt: 'Smartphone displaying QR code scan with modern tech aesthetic', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'QR code analytics empowers marketers to track scan rates, user behavior, and campaign ROI through real-time dashboards, enabling data-driven optimization of dynamic QR codes and branded marketing campaigns.', - howTo: { - name: 'How to Track QR Code Scans', - description: 'Step-by-step guide to setting up and monitoring QR code analytics', - totalTime: 'PT10M', - steps: [ - { - name: 'Create a Dynamic QR Code', - text: 'Log into your QR Master dashboard and select "Create Dynamic QR Code". Enter your destination URL and customize design options.', - url: 'https://www.qrmaster.net/create', - }, - { - name: 'Enable UTM Tracking', - text: 'Add UTM parameters (source, medium, campaign) to track the QR code in Google Analytics and marketing platforms.', - }, - { - name: 'Access Analytics Dashboard', - text: 'Navigate to Dashboard → Analytics to view real-time scan data, geographic distribution, and device breakdowns.', - url: 'https://www.qrmaster.net/analytics', - }, - ], - }, - content: `<div class="blog-content"> - <h2>What Are Scan Analytics?</h2> - <p>Scan analytics provide comprehensive insights into how users interact with your QR codes. Our advanced dashboard tracks scan analytics including geographic location, device types, scan timestamps, and user engagement patterns. For marketers running dynamic QR code campaigns, these insights are essential for understanding campaign tracking performance and optimizing conversion rates.</p> - <p>With branded QR codes deployed across print materials, event tickets, and business cards, scan analytics reveal which channels drive the highest engagement. Security features ensure all data collection is GDPR-compliant, protecting user privacy while delivering actionable campaign tracking insights.</p> - - <h2>How to Set Up QR Code Analytics</h2> - <h3>Step 1: Create a Dynamic QR Code</h3> - <p>Start by generating a dynamic QR code in your QR Master dashboard. Unlike static codes, dynamic QR codes allow you to update destination URLs and track every scan through our analytics platform.</p> - - <h3>Step 2: Enable Campaign Tracking</h3> - <p>Configure UTM parameters for your QR codes to integrate with Google Analytics and marketing automation platforms. UTM tracking allows you to attribute conversions, measure ROI, and segment campaign performance by source, medium, and campaign name.</p> - - <h3>Step 3: Access Your Analytics Dashboard</h3> - <p>Navigate to the scan analytics dashboard to view real-time reports. Monitor scan rates, geographic distribution, device breakdowns, and time-series data. Set up automated reports to track campaign tracking metrics over time.</p> - - <h3>Step 4: Optimize Based on Insights</h3> - <p>Use scan analytics to identify high-performing campaigns and optimize underperforming ones. A/B test different branded QR designs, placement strategies, and call-to-action messaging to maximize engagement and conversion rates.</p> - - <h2>Key Metrics in QR Code Analytics</h2> - <h3>Scan Rates and Volume</h3> - <p>Track total scans, unique scans, and scan velocity. Scan rates reveal campaign momentum and help identify viral growth patterns. Compare scan volumes across different branded QR variations to determine which designs perform best.</p> - - <h3>Geographic Distribution</h3> - <p>Understand where your audience is scanning from. Geographic analytics support localized marketing strategies and event tracking for conferences, trade shows, and retail activations.</p> - - <h3>Device and Browser Analytics</h3> - <p>Know whether users scan from iOS or Android devices, which browsers they use, and screen resolutions. This data informs mobile optimization strategies and ensures your landing pages deliver seamless experiences across all devices.</p> - - <h3>Time-Based Patterns</h3> - <p>Identify peak scanning hours, days of the week, and seasonal trends. Time-based analytics optimize campaign timing for email blasts, social media posts, and print QR deployments.</p> - - <h3>Conversion Tracking</h3> - <p>Measure downstream actions after the scan—form submissions, purchases, app downloads, or content engagement. Integrate with your CRM and marketing stack to attribute revenue to specific QR campaigns.</p> - - <div class="my-8"> - <img src="/blog/4-body.png" alt="Team meeting analyzing QR code data in office" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>Advanced Campaign Tracking Strategies</h2> - <h3>UTM Tracking Integration</h3> - <p>Append UTM parameters to your dynamic QR URLs for granular campaign attribution. Use consistent naming conventions across campaigns to compare performance in Google Analytics. UTM tracking bridges offline and online marketing, providing a unified view of customer journeys.</p> - - <h3>Multi-Channel Attribution</h3> - <p>Deploy branded QR codes across print ads, packaging, event tickets, business cards, and signage. Use unique QR codes for each channel to measure which touchpoints drive the highest ROI. Multi-channel attribution reveals the true value of integrated marketing campaigns.</p> - - <h3>A/B Testing QR Designs</h3> - <p>Test different branded QR styles—color schemes, logo placements, and call-to-action text—to optimize scan rates. Our analytics dashboard makes it easy to compare performance and roll out winning variations at scale.</p> - - <h3>Retargeting and Remarketing</h3> - <p>Leverage scan analytics to build retargeting audiences. Users who scan but don't convert can be re-engaged with display ads, email campaigns, and social media retargeting, boosting overall campaign ROI.</p> - - <h2>Security and Compliance in QR Analytics</h2> - <p>All QR Master scan analytics are GDPR-compliant, ensuring user data is collected, stored, and processed securely. We employ enterprise-grade security protocols to protect sensitive campaign data, making our platform ideal for bulk QR generation workflows in regulated industries.</p> - <p>Secure QR codes prevent unauthorized access and malicious redirects. Our platform includes link validation, SSL encryption, and fraud detection to maintain trust and protect your brand reputation.</p> - - <h2>Use Cases for QR Code Analytics</h2> - <h3>Event Tracking</h3> - <p>Deploy QR codes on event tickets, badges, and signage to track attendee engagement. Scan analytics reveal which sessions attract the most interest, optimize check-in flows, and measure event ROI.</p> - - <h3>Print Marketing Campaigns</h3> - <p>Use QR codes in magazine ads, direct mail, and packaging to bridge offline and online channels. Campaign tracking quantifies print campaign performance and justifies marketing spend.</p> - - <h3>Business Card Analytics</h3> - <p>Add dynamic QR codes to business cards to track networking effectiveness. Scan analytics show how many contacts engage, when they scan, and which follow-up actions they take.</p> - - <h3>Bulk QR Generation for Retail</h3> - <p>Generate thousands of product QR codes with our bulk QR tool. Track scan analytics at the SKU level to understand customer interest, optimize inventory, and personalize marketing.</p> - - <h3>API-Driven Automation</h3> - <p>Integrate QR code generation and analytics into your marketing automation platform via our API. Automate bulk QR creation, dynamic URL updates, and reporting workflows for enterprise-scale campaigns.</p> - - <h2>Maximizing ROI with Scan Analytics</h2> - <p>To maximize QR code ROI, continuously monitor scan analytics and iterate on campaign strategies. Test different branded QR designs, optimize UTM parameters, and leverage multi-channel attribution to understand the full customer journey.</p> - <p>Combine scan analytics with customer data platforms (CDPs) and CRMs to personalize follow-up communications. Segment audiences based on scan behavior and deliver targeted offers that drive conversions.</p> - <p>For bulk QR campaigns, use our analytics dashboard to identify trends across thousands of codes. Aggregate data reveals macro patterns while code-level metrics enable micro-optimizations.</p> - - <h2>Conclusion</h2> - <p>QR code analytics transforms QR codes from simple links into powerful marketing instruments. By tracking scan rates, user behavior, and campaign performance through advanced dashboards, marketers gain the insights needed to optimize dynamic QR campaigns, enhance branded experiences, and achieve measurable ROI.</p> - <p>Whether you're deploying QR codes for event tracking, print marketing, bulk generation, or API-driven automation, scan analytics provides the data foundation for smarter, more effective campaigns. Start leveraging QR analytics today to unlock the full potential of your QR marketing strategy.</p> - </div>`, - }, - 'qr-code-tracking-guide-2025': { - slug: 'qr-code-tracking-guide-2025', - title: 'QR Code Tracking: Complete Guide 2025 (Free Tools & Best Practices)', - excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools, setup Google Analytics, and measure ROI. Complete guide 2025.', - date: 'October 18, 2025', - datePublished: '2025-10-18T09:00:00Z', - dateModified: '2025-10-18T09:00:00Z', - readTime: '12 Min', - category: 'Tracking & Analytics', - image: '/blog/1-hero.png', - imageAlt: 'QR code tracking and analytics visualization', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'QR code tracking allows you to monitor scan metrics including location, device type, time, and user behavior using dynamic QR codes. Only dynamic QR codes can be tracked—static codes cannot provide analytics. Use tools like QR Master, Google Analytics with UTM parameters, or URL shorteners to track scans and measure campaign ROI effectively.', - howTo: { - name: 'How to Set Up QR Code Tracking', - description: 'Complete step-by-step guide to tracking QR code scans with analytics', - totalTime: 'PT15M', - steps: [ - { - name: 'Create a Dynamic QR Code', - text: 'Sign up for QR Master and create a dynamic QR code. Enter your destination URL and customize the design with your brand colors and logo.', - url: 'https://www.qrmaster.net/signup', - }, - { - name: 'Add UTM Parameters', - text: 'Configure UTM tracking parameters: utm_source=qr, utm_medium=print, utm_campaign=your-campaign-name. This enables tracking in Google Analytics.', - }, - { - name: 'Deploy Your QR Code', - text: 'Download the QR code and place it on your marketing materials: print ads, product packaging, business cards, or event posters.', - }, - { - name: 'Monitor Analytics Dashboard', - text: 'Access your QR Master dashboard to view real-time scan data: total scans, unique users, geographic location, device types, and scan timestamps.', - url: 'https://www.qrmaster.net/analytics', - }, - { - name: 'Optimize Based on Data', - text: 'Analyze scan patterns to optimize your campaigns. Test different placements, designs, and calls-to-action to improve scan rates and conversion.', - }, - ], - }, - content: `<div class="blog-content"> - <p>QR code tracking is essential for measuring the success of your marketing campaigns. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented in 1994 by Masahiro Hara at Denso Wave, and have evolved from automotive tracking to powerful marketing tools with advanced analytics capabilities. In this comprehensive guide, you'll learn everything about tracking QR code scans, from basic setup to advanced campaign optimization.</p> - - <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> - <h3 class="text-xl font-semibold mb-2 text-gray-900">Quick Takeaway</h3> - <p class="text-gray-800">Only <strong>dynamic QR codes</strong> can be tracked. Static QR codes encode data directly and provide no analytics. To track scans, you must use a dynamic QR code that redirects through a server that logs scan data. QR Master offers unlimited tracking with detailed analytics on every scan.</p> - </div> - - <h2>What is QR Code Tracking?</h2> - <p>QR code tracking is the process of monitoring and analyzing scan data from QR codes to measure campaign performance and user behavior. When someone scans a trackable QR code, the system captures valuable data including:</p> - - <ul> - <li><strong>Scan count:</strong> Total scans and unique scans</li> - <li><strong>Location data:</strong> Country, city, and region of the scanner</li> - <li><strong>Device information:</strong> iOS vs Android, device model, operating system version</li> - <li><strong>Time and date:</strong> When scans occur (hour, day, week, month)</li> - <li><strong>Referrer source:</strong> Where the scan originated (if tracked)</li> - <li><strong>User behavior:</strong> Actions taken after scanning (page views, conversions, purchases)</li> - </ul> - - <h3>Static vs Dynamic QR Codes: Why Tracking Matters</h3> - <p>Understanding the difference between static and dynamic QR codes is crucial for tracking:</p> - - <p><strong>Static QR Codes:</strong> These encode the destination URL directly into the QR code pattern. Once generated, the content cannot be changed, and no tracking is possible. The QR code reader goes directly to the encoded destination without any intermediate server.</p> - - <p><strong>Dynamic QR Codes:</strong> These contain a short redirect URL (like qrmaster.net/abc123) that points to a server. The server logs the scan data and then redirects to your actual destination URL. This enables tracking AND allows you to change the destination URL anytime—even after printing thousands of codes.</p> - - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Feature</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Static QR</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Dynamic QR</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Track Scans</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Edit After Printing</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Analytics Dashboard</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Cost</td> - <td class="border border-gray-300 px-6 py-4">Free</td> - <td class="border border-gray-300 px-6 py-4">Free - $29/month</td> - </tr> - </tbody> - </table> - </div> - - <h2>Why Track QR Codes? Key Benefits</h2> - - <h3>1. Measure Marketing ROI</h3> - <p>QR code tracking provides concrete data on campaign performance. Instead of guessing how many people engaged with your print ad, you get exact numbers. Calculate cost per scan: if your billboard costs $5,000/month and generates 10,000 scans, that's $0.50 per engagement—compare that to your digital ad costs.</p> - - <p><strong>Real-world example:</strong> A retail brand placed QR codes on product packaging linking to warranty registration. By tracking scans, they discovered only 12% of customers registered warranties. They A/B tested different incentives (15% off next purchase vs extended warranty) and increased registration to 34%—all measured through QR code tracking.</p> - - <h3>2. Understand Your Audience</h3> - <p>QR code analytics reveal WHO is scanning your codes:</p> - <ul> - <li><strong>Geographic insights:</strong> Are most scans from your local area or nationwide? This helps optimize ad placement.</li> - <li><strong>Device data:</strong> 70% iOS users might indicate an affluent audience; optimize your landing page accordingly.</li> - <li><strong>Time patterns:</strong> Peak scanning at 7-9 PM? Schedule your social media posts and email campaigns for those hours.</li> - </ul> - - <h3>3. Optimize Product Engagement</h3> - <p>For e-commerce and retail, QR codes on packaging track which products generate the most engagement. If Product A gets 5x more scans than Product B, you know customers are more interested in learning about Product A—perhaps it needs clearer instructions, or customers want to see user reviews.</p> - - <h3>4. Event Management & Attendance Tracking</h3> - <p>Event tickets with unique QR codes enable real-time attendance tracking. Scan QR codes at check-in to see who arrived, prevent duplicate entries, and measure session attendance. Post-event, analyze which sessions were most popular and optimize future events.</p> - - <h2>How to Track QR Code Scans: 4 Methods</h2> - - <h3>Method 1: Using Dynamic QR Code Generators (Recommended)</h3> - <p>The easiest method is using a dedicated QR code platform like QR Master. These services provide built-in tracking without any technical setup.</p> - - <h4>Step-by-Step with QR Master:</h4> - <ol> - <li><strong>Sign up for free:</strong> Create your QR Master account at <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li> - <li><strong>Create dynamic QR code:</strong> Click "Create QR Code" and select "Dynamic QR"</li> - <li><strong>Enter destination URL:</strong> Add the website, landing page, or content you want to link</li> - <li><strong>Customize design:</strong> Add your logo, brand colors, and custom frame</li> - <li><strong>Download and deploy:</strong> Get high-resolution PNG or SVG for print</li> - <li><strong>Access analytics:</strong> Go to Dashboard → Analytics to view real-time scan data</li> - </ol> - - <p><strong>Benefits:</strong> No coding required, instant setup, real-time dashboard, unlimited scans on paid plans, GDPR compliant.</p> - - <h3>Method 2: Google Analytics with UTM Parameters</h3> - <p>If you're already using Google Analytics, you can track QR codes using UTM parameters. This method works with both static and dynamic QR codes, but you won't get device-specific data—only what Google Analytics provides.</p> - - <h4>How to Set Up UTM Tracking:</h4> - <ol> - <li><strong>Build your UTM URL:</strong> Use Google's Campaign URL Builder</li> - <li><strong>Add parameters:</strong> - <ul> - <li>utm_source=qr</li> - <li>utm_medium=print (or offline, packaging, etc.)</li> - <li>utm_campaign=summer2025 (your campaign name)</li> - </ul> - </li> - <li><strong>Example:</strong> <code>https://yoursite.com?utm_source=qr&utm_medium=print&utm_campaign=summer2025</code></li> - <li><strong>Generate QR code:</strong> Create QR code from this UTM-tagged URL</li> - <li><strong>Track in Google Analytics:</strong> Go to Acquisition → Campaigns to view QR code traffic</li> - </ol> - - <p><strong>Limitations:</strong> No device-specific data, no real-time scan count, cannot edit URL after printing (unless you use dynamic QR codes WITH UTM parameters—best of both worlds).</p> - - <h3>Method 3: URL Shorteners with Analytics</h3> - <p>Services like Bitly, TinyURL, and Rebrandly offer URL shortening with basic analytics. Create a short link, generate a QR code from it, and track clicks in the shortener's dashboard.</p> - - <h4>Pros:</h4> - <ul> - <li>Free tier available (Bitly: 1,000 links free)</li> - <li>Simple setup, no coding</li> - <li>Basic analytics: click count, geographic data</li> - </ul> - - <h4>Cons:</h4> - <ul> - <li>Limited customization</li> - <li>Less detailed analytics than dedicated QR platforms</li> - <li>Cannot add logo or branding to QR code itself</li> - <li>Links might look generic (bit.ly/abc123) rather than branded</li> - </ul> - - <h3>Method 4: Self-Hosted Tracking (Advanced)</h3> - <p>For developers or enterprises with specific requirements, build your own QR redirect system. Use Node.js, PHP, Python, or serverless functions (AWS Lambda, Cloudflare Workers) to create a custom redirect server that logs scan data to your database.</p> - - <h4>Basic Implementation (Node.js Example):</h4> - <pre><code> -app.get('/qr/:id', async (req, res) => { - const qrId = req.params.id; - - // Log scan data - await logScan({ - qrId, - timestamp: new Date(), - ip: req.ip, - userAgent: req.get('user-agent'), - referrer: req.get('referer') - }); - - // Get destination URL from database - const destination = await getDestination(qrId); - - // Redirect to destination - res.redirect(302, destination); -}); - </code></pre> - - <p><strong>Privacy Note:</strong> Always hash IP addresses, respect Do Not Track headers, and comply with GDPR when collecting scan data.</p> - - <div class="my-8"> - <img src="/blog/1-boy.png" alt="Person scanning QR code with smartphone in office" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>QR Code Tracking Tools Comparison</h2> - - <h3>Free Tools:</h3> - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Tool</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Scans/Month</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Analytics</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Custom Domain</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Price</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Free</td> - <td class="border border-gray-300 px-6 py-4">Unlimited</td> - <td class="border border-gray-300 px-6 py-4">Full Dashboard</td> - <td class="border border-gray-300 px-6 py-4">No</td> - <td class="border border-gray-300 px-6 py-4">$0</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Google Analytics</td> - <td class="border border-gray-300 px-6 py-4">Unlimited</td> - <td class="border border-gray-300 px-6 py-4">Full (with GA4)</td> - <td class="border border-gray-300 px-6 py-4">Yes</td> - <td class="border border-gray-300 px-6 py-4">$0</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Bitly Free</td> - <td class="border border-gray-300 px-6 py-4">1,000</td> - <td class="border border-gray-300 px-6 py-4">Basic</td> - <td class="border border-gray-300 px-6 py-4">No</td> - <td class="border border-gray-300 px-6 py-4">$0</td> - </tr> - </tbody> - </table> - </div> - - <h3>Paid Tools:</h3> - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Tool</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Scans/Month</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Analytics</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Custom Domain</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Price</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Pro</td> - <td class="border border-gray-300 px-6 py-4">Unlimited</td> - <td class="border border-gray-300 px-6 py-4">Advanced</td> - <td class="border border-gray-300 px-6 py-4">Yes</td> - <td class="border border-gray-300 px-6 py-4">$9/mo</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Code Generator</td> - <td class="border border-gray-300 px-6 py-4">Unlimited</td> - <td class="border border-gray-300 px-6 py-4">Full</td> - <td class="border border-gray-300 px-6 py-4">Yes</td> - <td class="border border-gray-300 px-6 py-4">$12/mo</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Beaconstac</td> - <td class="border border-gray-300 px-6 py-4">Unlimited</td> - <td class="border border-gray-300 px-6 py-4">Advanced</td> - <td class="border border-gray-300 px-6 py-4">Yes</td> - <td class="border border-gray-300 px-6 py-4">$49/mo</td> - </tr> - </tbody> - </table> - </div> - - <p><strong>Our Recommendation:</strong></p> - <ul> - <li><strong>For beginners:</strong> Start with QR Master Free + Google Analytics</li> - <li><strong>For small businesses:</strong> QR Master Pro at $9/month (best value)</li> - <li><strong>For enterprises:</strong> Beaconstac or custom solution</li> - </ul> - - <h2>QR Code Tracking Best Practices</h2> - - <h3>1. Always Use Dynamic QR Codes for Campaigns</h3> - <p>Static QR codes cannot be tracked or edited. If you're printing 1,000+ codes or spending significant money on the campaign, always use dynamic QR codes. The small monthly cost ($9-29) is negligible compared to reprint costs if the URL changes.</p> - - <h3>2. Set Clear Goals Before Tracking</h3> - <p>Define what success looks like before launching your campaign:</p> - <ul> - <li>Target scan count: "We want 500+ scans in the first month"</li> - <li>Conversion goal: "30% of scanners should sign up for newsletter"</li> - <li>Geographic goal: "Focus on scans from NYC metro area"</li> - </ul> - - <h3>3. Use Consistent UTM Naming Conventions</h3> - <p>Standardize your UTM parameters across all QR codes:</p> - <ul> - <li>utm_source: Always "qr"</li> - <li>utm_medium: Specific placement ("billboard", "packaging", "flyer", "business-card")</li> - <li>utm_campaign: Campaign name ("summer2025", "product-launch", "event-ticket")</li> - </ul> - <p>Example: <code>utm_source=qr&utm_medium=billboard&utm_campaign=summer2025</code></p> - - <h3>4. Test Before Printing</h3> - <p>Before sending 10,000 codes to the printer:</p> - <ul> - <li>Scan QR code with multiple devices (iOS, Android)</li> - <li>Verify tracking is working in your dashboard</li> - <li>Check landing page loads fast on mobile (<3 seconds)</li> - <li>Test different lighting conditions and distances</li> - </ul> - - <h3>5. Monitor Regularly</h3> - <p>Don't just "set and forget" your QR codes:</p> - <ul> - <li><strong>Daily:</strong> Check for the first week to catch any issues early</li> - <li><strong>Weekly:</strong> Review scan trends during active campaigns</li> - <li><strong>Monthly:</strong> Analyze long-term patterns and create reports</li> - </ul> - - <h3>6. Privacy & GDPR Compliance</h3> - <p>Respect user privacy when collecting scan data:</p> - <ul> - <li><strong>Hash IP addresses:</strong> Don't store raw IPs; hash them for privacy</li> - <li><strong>Respect Do Not Track:</strong> Honor DNT browser headers</li> - <li><strong>Privacy policy:</strong> Mention QR tracking in your privacy policy</li> - <li><strong>GDPR compliance:</strong> Allow EU users to request data deletion</li> - <li><strong>Cookie consent:</strong> If your landing page uses cookies, show consent banner</li> - </ul> - - <h2>QR Code Tracking Use Cases</h2> - - <h3>1. Restaurant Menus</h3> - <p><strong>Scenario:</strong> Restaurant replaces physical menus with QR codes on tables.</p> - <p><strong>Tracking Benefits:</strong></p> - <ul> - <li>See which tables generate most scans (optimize table placement)</li> - <li>Track peak scanning times (staff accordingly)</li> - <li>A/B test different menu designs to increase order value</li> - <li>Measure return scan rate (customer loyalty indicator)</li> - </ul> - - <h3>2. Retail Product Packaging</h3> - <p><strong>Scenario:</strong> Product packaging includes QR code linking to warranty registration.</p> - <p><strong>Tracking Benefits:</strong></p> - <ul> - <li>Track which products have highest engagement</li> - <li>Measure warranty registration completion rate</li> - <li>Identify geographic markets with strong sales</li> - <li>A/B test incentives (discount codes vs extended warranty)</li> - </ul> - - <h3>3. Event Tickets</h3> - <p><strong>Scenario:</strong> Conference tickets feature unique QR codes for check-in.</p> - <p><strong>Tracking Benefits:</strong></p> - <ul> - <li>Real-time attendance tracking</li> - <li>Prevent duplicate check-ins (fraud prevention)</li> - <li>Track session attendance by placing QR at session doors</li> - <li>Post-event analysis: which sessions were most popular?</li> - </ul> - - <h3>4. Real Estate Signs</h3> - <p><strong>Scenario:</strong> For Sale signs include QR code to property details.</p> - <p><strong>Tracking Benefits:</strong></p> - <ul> - <li>Measure property interest (scan count = qualified leads)</li> - <li>Track which neighborhoods generate most interest</li> - <li>Capture leads automatically (link to contact form)</li> - <li>A/B test different signage designs</li> - </ul> - - <h3>5. Print Advertising</h3> - <p><strong>Scenario:</strong> Magazine ad includes QR code to special offer.</p> - <p><strong>Tracking Benefits:</strong></p> - <ul> - <li>Calculate cost per scan (ad cost Ă· scans)</li> - <li>Compare performance across different magazines</li> - <li>Track which ad creative generates most scans</li> - <li>Measure conversion rate from scan to purchase</li> - </ul> - - <h2>Advanced QR Code Tracking Strategies</h2> - - <h3>A/B Testing QR Code Designs</h3> - <p>Don't just guess which QR design performs best—test it. Create two versions:</p> - <ul> - <li><strong>Version A:</strong> Plain black and white QR</li> - <li><strong>Version B:</strong> Branded QR with logo and custom colors</li> - </ul> - <p>Deploy equal quantities of each and track which generates more scans. Many brands find that branded QR codes get 30-50% higher scan rates because they look more trustworthy.</p> - - <h3>Multi-Channel Attribution</h3> - <p>Use unique QR codes for each marketing channel to measure which drives the best results:</p> - <ul> - <li>Billboard: <code>qrmaster.net/billboard-nyc</code></li> - <li>Magazine ad: <code>qrmaster.net/magazine-vogue</code></li> - <li>Product packaging: <code>qrmaster.net/packaging-productA</code></li> - <li>Business card: <code>qrmaster.net/card-john</code></li> - </ul> - <p>Track scans separately to calculate ROI per channel.</p> - - <h3>Retargeting Scanners Who Don't Convert</h3> - <p>Add retargeting pixels (Facebook Pixel, Google Ads remarketing) to your QR code landing page. Users who scan but don't convert can be retargeted with ads:</p> - <ul> - <li>"Still interested? Get 15% off today"</li> - <li>"You left something in your cart..."</li> - <li>"Here's what you were looking for"</li> - </ul> - <p>This dramatically improves overall campaign ROI.</p> - - <h3>Geographic Targeting</h3> - <p>If QR tracking shows 70% of scans come from California, optimize your campaigns:</p> - <ul> - <li>Show California-specific content on landing page</li> - <li>Offer California-only promotions</li> - <li>Increase ad spend in California, decrease elsewhere</li> - <li>Open physical retail in high-scan regions</li> - </ul> - - <h2>Common QR Code Tracking Mistakes to Avoid</h2> - - <h3>Mistake 1: Using Static QR Codes for Campaigns</h3> - <p>Static QR codes cannot be tracked or edited. If you print 5,000 flyers with a static QR and the URL changes, you're stuck. Always use dynamic QR codes for any quantity over 100.</p> - - <h3>Mistake 2: Not Mobile-Optimizing Landing Pages</h3> - <p>100% of QR code scans come from mobile devices. If your landing page isn't mobile-friendly, you'll lose 50-70% of potential conversions. Test on real devices before launching.</p> - - <h3>Mistake 3: Ignoring Privacy Regulations</h3> - <p>Collecting scan data without proper consent can result in GDPR fines up to €20 million. Always:</p> - <ul> - <li>Include QR tracking in your privacy policy</li> - <li>Obtain consent for cookies on landing page</li> - <li>Allow users to opt-out of tracking</li> - <li>Hash or anonymize IP addresses</li> - </ul> - - <h3>Mistake 4: Setting Unrealistic Scan Goals</h3> - <p>Typical QR code scan rates:</p> - <ul> - <li><strong>Business cards:</strong> 5-15% scan rate</li> - <li><strong>Product packaging:</strong> 1-5% scan rate</li> - <li><strong>Restaurant tables:</strong> 30-70% scan rate (motivated users want menu)</li> - <li><strong>Print ads:</strong> 0.5-2% scan rate</li> - </ul> - <p>Set goals based on industry benchmarks, not wishful thinking.</p> - - <h3>Mistake 5: Not Testing Before Printing</h3> - <p>One small mistake—wrong URL, broken link, slow loading page—can ruin an entire campaign. Always test:</p> - <ul> - <li>Scan from iOS and Android</li> - <li>Verify destination URL is correct</li> - <li>Check mobile page load speed (<3 seconds)</li> - <li>Confirm tracking is working in dashboard</li> - </ul> - - <h2>QR Code Tracking Metrics to Monitor</h2> - - <h3>Primary Metrics:</h3> - <ul> - <li><strong>Total scans:</strong> Raw number of all scans</li> - <li><strong>Unique scans:</strong> Number of individual users (more important than total)</li> - <li><strong>Scan rate:</strong> Scans Ă· potential impressions</li> - <li><strong>Conversion rate:</strong> Conversions Ă· scans</li> - <li><strong>Cost per scan:</strong> Campaign cost Ă· total scans</li> - <li><strong>ROI:</strong> (Revenue - Cost) Ă· Cost × 100%</li> - </ul> - - <h3>Secondary Metrics:</h3> - <ul> - <li><strong>Geographic distribution:</strong> Where scanners are located</li> - <li><strong>Device breakdown:</strong> iOS vs Android percentage</li> - <li><strong>Time patterns:</strong> Peak scanning hours/days</li> - <li><strong>Referrer data:</strong> How users discovered the QR code</li> - <li><strong>Bounce rate:</strong> % who leave immediately after scanning</li> - <li><strong>Average session duration:</strong> Time spent on landing page</li> - </ul> - - <h2>Conclusion</h2> - <p>QR code tracking transforms simple codes into powerful marketing instruments. By implementing the strategies in this guide—using dynamic QR codes, setting up proper analytics, following best practices, and continuously optimizing—you can measure and improve campaign ROI dramatically.</p> - - <p>Remember:</p> - <ul> - <li>Always use <strong>dynamic QR codes</strong> for tracking (static cannot be tracked)</li> - <li>Start with <strong>free tools</strong> like QR Master Free + Google Analytics</li> - <li>Set <strong>clear goals</strong> before launching campaigns</li> - <li>Test <strong>thoroughly</strong> before printing large quantities</li> - <li>Monitor <strong>regularly</strong> and optimize based on data</li> - <li>Respect <strong>privacy regulations</strong> (GDPR, CCPA)</li> - </ul> - - <p>Whether you're tracking restaurant menus, product packaging, event tickets, or print advertising, QR code analytics provides the insights needed to justify marketing spend and improve performance. Start tracking your QR codes today and unlock data-driven marketing success.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Ready to Start Tracking?</h3> - <p class="text-lg text-gray-700 mb-6">Create your first trackable dynamic QR code in 60 seconds. Free plan includes unlimited scans with full analytics dashboard.</p> - <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create Free QR Code Now →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a> - Create trackable QR codes</li> - <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a> - Learn the difference</li> - <li><a href="/blog/bulk-qr-code-generator-excel">Bulk QR Code Generator</a> - Generate hundreds at once</li> - <li><a href="/pricing">Pricing Plans</a> - Compare free and paid options</li> - <li><a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">QR Code on Wikipedia</a> - Technical details and history</li> - </ul> - </div>`, - }, - 'dynamic-vs-static-qr-codes': { - slug: 'dynamic-vs-static-qr-codes', - title: 'Dynamic vs Static QR Codes: Which Should You Use? Complete Comparison 2025', - excerpt: 'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money. Expert guide 2025.', - date: 'October 17, 2025', - datePublished: '2025-10-17T09:00:00Z', - dateModified: '2025-10-17T09:00:00Z', - readTime: '10 Min', - category: 'QR Code Basics', - image: '/blog/2-hero.png', - imageAlt: 'Two QR codes side by side showing static and dynamic comparison', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'Static QR codes encode data directly and cannot be edited after creation, while dynamic QR codes contain a short redirect URL that can be updated anytime. Dynamic QR codes also provide tracking analytics, making them ideal for marketing campaigns. Static QR codes work forever without subscriptions, perfect for permanent content like contact cards or fixed URLs.', - content: `<div class="blog-content"> - <p>Choosing between static and dynamic QR codes is one of the most important decisions when implementing a QR code strategy. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented in 1994 by Masahiro Hara at Denso Wave for automotive part tracking. Today, QR codes have evolved into sophisticated marketing tools, with dynamic QR codes offering features unimaginable in their original static form.</p> - - <p>This comprehensive guide explains the critical differences between static and dynamic QR codes, helping you choose the right type for your specific needs. Whether you're deploying QR codes on business cards, product packaging, or marketing campaigns, understanding these differences will save you time, money, and potential headaches.</p> - - <h2>What is a Static QR Code?</h2> - - <p>A static QR code directly encodes your data into the QR code pattern itself. When you create a static QR code for a URL, that URL is permanently embedded in the black-and-white squares. The QR code reader decodes the pattern and accesses the content directly—no intermediate server, no redirect, no tracking.</p> - - <h3>How Static QR Codes Work</h3> - <p>Think of a static QR code like printing a phone number on a business card. The phone number is the final information—there's no lookup service or translation layer. When someone scans the QR code, their device reads the encoded data and immediately processes it (opens the URL, displays the text, opens a location in maps, etc.).</p> - - <p><strong>Example:</strong> If you create a static QR code for <code>https://www.yourwebsite.com/summer-sale-2025</code>, that exact URL is encoded into the QR code pattern. The QR code scanner extracts this URL and opens it directly.</p> - - <h3>Common Uses for Static QR Codes</h3> - <ul> - <li><strong>Contact cards (vCard):</strong> Share permanent contact information on business cards</li> - <li><strong>Location links:</strong> Direct links to Google Maps locations for offices or stores</li> - <li><strong>App store links:</strong> Fixed URLs that never change</li> - <li><strong>Bitcoin wallet addresses:</strong> Cryptocurrency payment addresses</li> - <li><strong>Fixed website URLs:</strong> Company homepage, about page, etc.</li> - <li><strong>Text messages or phone numbers:</strong> "Text HELP to 12345"</li> - </ul> - - <h3>Advantages of Static QR Codes</h3> - <div class="bg-green-50 border-l-4 border-green-500 p-6 my-6 rounded-r-lg"> - <ul> - <li><strong>✅ Works forever:</strong> No dependency on external servers or subscriptions. Once created, it functions permanently.</li> - <li><strong>✅ Faster scanning:</strong> No redirect delay—scanner goes directly to content (typically 100-300ms faster than dynamic).</li> - <li><strong>✅ Works offline:</strong> For content types like vCards or location data, no internet connection needed for initial scan.</li> - <li><strong>✅ Completely free:</strong> No ongoing costs or subscriptions required.</li> - <li><strong>✅ Privacy-friendly:</strong> No tracking, no data collection, no third-party involvement.</li> - <li><strong>✅ Simple:</strong> What you encode is what you get—no complexity.</li> - </ul> - </div> - - <h3>Disadvantages of Static QR Codes</h3> - <div class="bg-red-50 border-l-4 border-red-500 p-6 my-6 rounded-r-lg"> - <ul> - <li><strong>❌ Cannot edit after printing:</strong> If the URL changes or contains a typo, you must reprint all QR codes.</li> - <li><strong>❌ No analytics:</strong> Impossible to track scan count, location, device, or user behavior.</li> - <li><strong>❌ Long URLs create complex codes:</strong> Longer URLs = more data = denser, harder-to-scan QR codes.</li> - <li><strong>❌ No A/B testing:</strong> Cannot test different destinations without creating multiple QR codes.</li> - <li><strong>❌ No expiration dates:</strong> Cannot set codes to stop working after a certain date.</li> - <li><strong>❌ No password protection:</strong> Anyone with the QR code can access the content.</li> - </ul> - </div> - - <h3>Visual Example: Static QR Code Data Flow</h3> - <pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto"> -Static QR Code Content: -https://www.example.com/products/widget-a?ref=print-ad-2025 - -User Scans QR Code - ↓ -QR Scanner Decodes Pattern - ↓ -Opens: https://www.example.com/products/widget-a?ref=print-ad-2025 - ↓ -No Tracking | Cannot Edit | Works Forever - </pre> - - <h2>What is a Dynamic QR Code?</h2> - - <p>A dynamic QR code contains a short redirect URL instead of your actual content. This short URL points to a server that logs the scan data and then redirects to your final destination URL. The key advantage: you can change the destination URL anytime from your dashboard without reprinting the QR code.</p> - - <h3>How Dynamic QR Codes Work</h3> - <p>Think of a dynamic QR code like a phone forwarding service. When someone calls your forwarding number (the short URL in the QR code), the service logs the call and forwards it to your real phone (the destination URL). You can change your real phone number anytime without changing the forwarding number people dial.</p> - - <p><strong>Example:</strong> A dynamic QR code might contain <code>qrmaster.net/abc123</code>. When scanned, this redirects to your actual URL: <code>https://www.yourwebsite.com/summer-sale-2025</code>. Later, you can change it to <code>https://www.yourwebsite.com/fall-sale-2025</code> without reprinting.</p> - - <h3>Common Uses for Dynamic QR Codes</h3> - <ul> - <li><strong>Marketing campaigns:</strong> Print ads, billboards, posters where offers change</li> - <li><strong>Product packaging:</strong> Link to manuals that get updated</li> - <li><strong>Event tickets:</strong> Event details that might change</li> - <li><strong>Business cards:</strong> Update your website or portfolio without reprinting cards</li> - <li><strong>Restaurant menus:</strong> Daily specials and seasonal menu updates</li> - <li><strong>Retail displays:</strong> Promotions that change weekly or monthly</li> - </ul> - - <h3>Advantages of Dynamic QR Codes</h3> - <div class="bg-green-50 border-l-4 border-green-500 p-6 my-6 rounded-r-lg"> - <ul> - <li><strong>✅ Edit destination anytime:</strong> Change URL without reprinting QR codes—save thousands in reprint costs.</li> - <li><strong>✅ Full analytics:</strong> Track scans, geographic location, device types, time patterns, and user behavior.</li> - <li><strong>✅ A/B testing:</strong> Test different landing pages to optimize conversion rates.</li> - <li><strong>✅ Short, clean URLs:</strong> QR code contains <code>qrmaster.net/abc123</code> instead of long ugly URLs.</li> - <li><strong>✅ Set expiration dates:</strong> Configure codes to stop working after campaigns end.</li> - <li><strong>✅ Password protection:</strong> Require password to access destination content.</li> - <li><strong>✅ Retargeting pixels:</strong> Add Facebook Pixel, Google Ads tracking for remarketing.</li> - <li><strong>✅ Scheduled redirects:</strong> Change destination based on time/date automatically.</li> - </ul> - </div> - - <h3>Disadvantages of Dynamic QR Codes</h3> - <div class="bg-red-50 border-l-4 border-red-500 p-6 my-6 rounded-r-lg"> - <ul> - <li><strong>❌ Depends on service provider:</strong> If the service shuts down or your subscription lapses, QR codes stop working.</li> - <li><strong>❌ Subscription required:</strong> Most services charge $5-50/month for advanced features (though QR Master Free offers 3 codes free).</li> - <li><strong>❌ Slight redirect delay:</strong> Extra 50-200ms for server redirect (usually imperceptible).</li> - <li><strong>❌ Requires internet:</strong> Cannot work offline (though the destination can be cached).</li> - <li><strong>❌ Privacy concerns:</strong> Service provider can see scan data (choose GDPR-compliant providers).</li> - </ul> - </div> - - <h3>Visual Example: Dynamic QR Code Data Flow</h3> - <pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto"> -Dynamic QR Code Content: -qrmaster.net/abc123 - -User Scans QR Code - ↓ -QR Scanner Decodes Pattern - ↓ -Contacts: qrmaster.net/abc123 - ↓ -Server Logs: Device, Location, Time, User Agent - ↓ -Redirects to: https://www.example.com/current-promotion - ↓ -Tracking ✓ | Editable ✓ | Analytics ✓ - </pre> - - <div class="my-8"> - <img src="/blog/2-body.png" alt="Business card with elegant QR code" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>Static vs Dynamic QR Codes: Side-by-Side Comparison</h2> - - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Feature</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Static QR Code</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Dynamic QR Code</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Edit After Printing</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Track Scans</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Analytics Dashboard</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">A/B Testing</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">URL Length in QR</td> - <td class="border border-gray-300 px-6 py-4">Long (full URL)</td> - <td class="border border-gray-300 px-6 py-4">Short (redirect URL)</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Cost</td> - <td class="border border-gray-300 px-6 py-4">Free (forever)</td> - <td class="border border-gray-300 px-6 py-4">Free - $50/month</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Works Forever</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - <td class="border border-gray-300 px-6 py-4">Depends on subscription</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Redirect Speed</td> - <td class="border border-gray-300 px-6 py-4">Instant</td> - <td class="border border-gray-300 px-6 py-4">50-200ms delay</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Privacy</td> - <td class="border border-gray-300 px-6 py-4">High (no tracking)</td> - <td class="border border-gray-300 px-6 py-4">Lower (tracked)</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Expiration Date</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Password Protection</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Best For</td> - <td class="border border-gray-300 px-6 py-4">Fixed, permanent content</td> - <td class="border border-gray-300 px-6 py-4">Marketing campaigns</td> - </tr> - </tbody> - </table> - </div> - - <h2>Real-World Scenarios: Static vs Dynamic</h2> - - <h3>When Static QR Codes Excel</h3> - - <h4>Scenario 1: Business Card Contact Information</h4> - <p><strong>Situation:</strong> Professional wants to share their contact details easily at networking events.</p> - <p><strong>Why Static:</strong> Contact information rarely changes. No tracking needed for personal cards. QR code works forever even if you stop paying for QR service. Privacy-friendly (no data collection).</p> - <p><strong>Cost Savings:</strong> $0 forever vs $5-15/month for dynamic QR service = $60-180/year saved.</p> - - <h4>Scenario 2: Book Back Cover</h4> - <p><strong>Situation:</strong> Author wants QR code on book cover linking to their website.</p> - <p><strong>Why Static:</strong> Author's website URL is unlikely to change. Book will be in print for years. No need to track individual reader scans. Permanent, reliable link.</p> - - <h4>Scenario 3: Memorial Plaque</h4> - <p><strong>Situation:</strong> Memorial plaque with QR code linking to person's biography.</p> - <p><strong>Why Static:</strong> Content is permanent. No tracking appropriate for memorial. Must work for decades without depending on subscription service. Ultimate longevity required.</p> - - <h3>When Dynamic QR Codes Are Essential</h3> - - <h4>Scenario 1: Billboard Advertising Campaign</h4> - <p><strong>Situation:</strong> Company runs 3-month billboard campaign with seasonal offer.</p> - <p><strong>Why Dynamic:</strong> Offer changes monthly. Must track which billboard locations perform best. ROI calculation requires scan data. A/B test different landing pages. After campaign ends, redirect to general website instead of expired offer.</p> - <p><strong>Cost Justification:</strong> Billboard costs $5,000/month. QR service costs $9/month. Tracking data helps optimize $15,000 campaign spend—easily worth $27 for analytics.</p> - - <h4>Scenario 2: Product Packaging (10,000 Units)</h4> - <p><strong>Situation:</strong> Manufacturing 10,000 product boxes with QR code to user manual PDF.</p> - <p><strong>Why Dynamic:</strong> Manual might get updated (typo corrections, new features, safety warnings). Cannot recall 10,000 products if URL changes. Need to track which regions/stores have highest engagement. Reprint cost is $5,000+ vs $9/month dynamic QR service.</p> - - <h4>Scenario 3: Business Cards for Consultant</h4> - <p><strong>Situation:</strong> Printing 500 business cards with QR code to portfolio.</p> - <p><strong>Why Dynamic:</strong> Portfolio website URL might change (rebranding, new domain). Can track which networking events drive most scans. Update QR to point to specific landing page for each prospect. Add new projects without reprinting cards.</p> - - <h2>When to Use Static QR Codes</h2> - - <p>Choose static QR codes when:</p> - - <h3>1. Content Never Changes</h3> - <ul> - <li>Contact information (vCard) that remains constant</li> - <li>App store download link (Apple App Store / Google Play URLs are stable)</li> - <li>Company homepage that's been the same for years</li> - <li>Historical information (museum exhibits, memorial plaques)</li> - </ul> - - <h3>2. Privacy is Critical</h3> - <ul> - <li>Personal contact information (vCard)</li> - <li>Sensitive documents where tracking is inappropriate</li> - <li>Legal/compliance scenarios where data collection is restricted</li> - <li>Medical information (HIPAA compliance concerns)</li> - </ul> - - <h3>3. Long-Term Reliability Needed</h3> - <ul> - <li>Museum exhibits (must work for decades)</li> - <li>Book publications (no ongoing subscription acceptable)</li> - <li>Historical markers or public art installations</li> - <li>Gravestones/memorial markers (ultimate permanence)</li> - </ul> - - <h3>4. Offline Content</h3> - <ul> - <li>vCard contact information (stored locally on device)</li> - <li>Location coordinates (opens maps app directly)</li> - <li>Plain text messages or instructions</li> - <li>SMS or phone number links</li> - </ul> - - <h3>5. Budget is $0</h3> - <ul> - <li>Personal projects with no funding</li> - <li>Small nonprofits with zero marketing budget</li> - <li>One-time events with no tracking needs</li> - <li>Side projects and hobby uses</li> - </ul> - - <h2>When to Use Dynamic QR Codes</h2> - - <p>Choose dynamic QR codes when:</p> - - <h3>1. Content Might Change</h3> - <ul> - <li>Seasonal promotions (summer sale → fall sale)</li> - <li>Product manuals that get updated</li> - <li>Event schedules or venue information</li> - <li>Restaurant menus with changing prices/items</li> - <li>Portfolio or resume links that evolve</li> - </ul> - - <h3>2. Tracking is Important</h3> - <ul> - <li>Marketing campaigns (need to measure ROI)</li> - <li>Product engagement (which products get scanned most?)</li> - <li>Event attendance (check-in tracking)</li> - <li>Print advertising (cost per scan analysis)</li> - <li>Geographic targeting (where are scans coming from?)</li> - </ul> - - <h3>3. A/B Testing Needed</h3> - <ul> - <li>Test different landing pages without reprinting</li> - <li>Optimize offers based on scan conversion data</li> - <li>Compare two different CTAs (call-to-action messages)</li> - <li>Experiment with different promotional strategies</li> - </ul> - - <h3>4. Large Print Run</h3> - <ul> - <li>50,000+ product labels (cannot afford reprint if URL changes)</li> - <li>10,000+ event posters (investment protection)</li> - <li>1,000+ business cards (future-proofing)</li> - <li>Any quantity where reprint cost > dynamic QR subscription cost</li> - </ul> - - <h3>5. Professional Marketing</h3> - <ul> - <li>Billboard advertising (high cost requires tracking)</li> - <li>Magazine ads (attribution to specific publications)</li> - <li>Product packaging (ongoing engagement measurement)</li> - <li>Retail displays (optimize based on performance data)</li> - <li>Direct mail campaigns (response rate tracking)</li> - </ul> - - <h2>Cost Analysis: Static vs Dynamic</h2> - - <h3>Static QR Code Total Cost</h3> - <p><strong>Creation:</strong> $0<br> - <strong>Maintenance:</strong> $0/month<br> - <strong>Lifetime Cost (10 years):</strong> $0</p> - - <p><strong>Risk:</strong> If URL changes, must reprint all materials. For 10,000 business cards: $200-500 reprint cost.</p> - - <h3>Dynamic QR Code Total Cost</h3> - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Provider</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Free Tier</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Pro Tier</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Enterprise</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Master</td> - <td class="border border-gray-300 px-6 py-4">3 codes free</td> - <td class="border border-gray-300 px-6 py-4">$9/mo (50 codes)</td> - <td class="border border-gray-300 px-6 py-4">$29/mo (500 codes)</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Code Generator</td> - <td class="border border-gray-300 px-6 py-4">1 code (trial)</td> - <td class="border border-gray-300 px-6 py-4">$12/mo</td> - <td class="border border-gray-300 px-6 py-4">$50/mo</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">Beaconstac</td> - <td class="border border-gray-300 px-6 py-4">Trial only</td> - <td class="border border-gray-300 px-6 py-4">$49/mo</td> - <td class="border border-gray-300 px-6 py-4">$199/mo</td> - </tr> - </tbody> - </table> - </div> - - <h3>Break-Even Analysis: 10,000 Business Cards</h3> - - <p><strong>Option A: Static QR Code</strong></p> - <ul> - <li>Print cost: $200</li> - <li>QR code service: $0</li> - <li>Total: $200</li> - <li>Risk: If URL changes, reprint costs another $200</li> - </ul> - - <p><strong>Option B: Dynamic QR Code</strong></p> - <ul> - <li>Print cost: $200</li> - <li>QR service: $9/month × 12 months = $108/year</li> - <li>Total Year 1: $308</li> - <li>Benefit: Update URL anytime, track all scans, optimize campaigns</li> - </ul> - - <p><strong>Conclusion:</strong> If there's ANY chance the URL might change, dynamic QR saves money. Reprint cost ($200) > annual dynamic service cost ($108).</p> - - <h3>Cost Recommendation by Print Quantity</h3> - <ul> - <li><strong>1-100 prints:</strong> Static is fine (low reprint cost)</li> - <li><strong>100-1,000 prints:</strong> Dynamic if content might change or tracking needed</li> - <li><strong>1,000+ prints:</strong> Always use dynamic (reprint cost too high to risk)</li> - </ul> - - <h2>Can You Convert Static to Dynamic?</h2> - - <p><strong>Short answer: No.</strong></p> - - <p>Once a static QR code is created and printed, you cannot convert it to dynamic. The data is permanently encoded in the QR pattern. The only solution is to reprint with a new dynamic QR code.</p> - - <h3>Migration Strategy</h3> - <p>If you have existing static QR codes that need updating:</p> - - <ol> - <li><strong>Domain redirect:</strong> If you control the domain, set up a server-side redirect from the static URL to the new destination.</li> - <li><strong>Phased replacement:</strong> Gradually replace static codes with dynamic as you reprint materials.</li> - <li><strong>Reprint high-value materials first:</strong> Billboard ads and product packaging first; business cards later.</li> - </ol> - - <h3>Prevention: Plan Ahead</h3> - <p>If there's ANY chance you'll need to edit the URL in the future:</p> - <ul> - <li>Start with dynamic QR codes from day one</li> - <li>Cost is minimal ($9-29/month) compared to reprint expenses</li> - <li>Better to have the flexibility and not need it than need it and not have it</li> - </ul> - - <h2>Special Cases: Wikipedia and QRpedia Example</h2> - - <p>An interesting real-world example of dynamic QR codes is <a href="https://meta.wikimedia.org/wiki/QRpedia" target="_blank" rel="noopener">QRpedia</a>, a system created by Wikipedia. QRpedia generates QR codes that link to Wikipedia articles. When scanned, the system:</p> - - <ol> - <li>Detects the scanner's language preference</li> - <li>Redirects to the Wikipedia article in that language</li> - <li>Falls back to English if the article doesn't exist in the user's language</li> - </ol> - - <p>This is only possible with dynamic QR codes. A static QR code would have to link to a single language version, reducing accessibility for international visitors. QRpedia demonstrates how dynamic QR codes enable sophisticated, user-personalized experiences.</p> - - <h2>Decision Tree: Static or Dynamic?</h2> - - <pre class="bg-gray-100 p-6 rounded-lg my-6 overflow-x-auto"> -Will the destination URL ever change? - ├─> YES → Use Dynamic QR Code - └─> NO → Do you need scan tracking/analytics? - ├─> YES → Use Dynamic QR Code - └─> NO → Is budget absolutely $0? - ├─> YES → Use Static QR Code - └─> NO → Consider Dynamic for future flexibility - </pre> - - <h2>Best Practices: Choosing Static vs Dynamic</h2> - - <h3>1. Default to Dynamic for Business Use</h3> - <p>When in doubt, choose dynamic. The cost is minimal ($9-29/month), and the flexibility protects your investment. Even if you don't think you'll need to change the URL, business circumstances change—mergers, rebrands, website migrations all happen.</p> - - <h3>2. Use Static for Personal, Permanent Content</h3> - <p>Personal projects, hobby websites, memorial content, and truly permanent information can safely use static QR codes. No ongoing cost, works forever, privacy-friendly.</p> - - <h3>3. Calculate the Reprint Cost</h3> - <p>Before deciding, calculate: "What would it cost to reprint if I'm wrong?" If reprint cost > 2 years of dynamic service, go dynamic.</p> - - <h3>4. Consider the Print Quantity</h3> - <ul> - <li>1-10 prints: Static is fine</li> - <li>10-100 prints: Consider dynamic if content might change</li> - <li>100-1,000 prints: Probably dynamic</li> - <li>1,000+ prints: Definitely dynamic</li> - </ul> - - <h3>5. Think About the Time Horizon</h3> - <ul> - <li>Short-term campaign (1-6 months): Dynamic essential</li> - <li>Medium-term (6 months - 2 years): Dynamic recommended</li> - <li>Long-term (2-5 years): Dynamic unless content truly permanent</li> - <li>Permanent (5+ years): Static acceptable if content won't change</li> - </ul> - - <h2>Conclusion</h2> - - <p>The choice between static and dynamic QR codes fundamentally comes down to two questions:</p> - - <ol> - <li><strong>Will the destination ever need to change?</strong></li> - <li><strong>Do you need scan tracking and analytics?</strong></li> - </ol> - - <p>If you answered "yes" to either question, dynamic QR codes are the clear choice. For truly permanent content with zero tracking needs and zero budget, static QR codes remain a perfectly valid option.</p> - - <p>Most businesses will benefit from dynamic QR codes. The ability to update destinations, track campaign performance, and A/B test landing pages provides enormous value. At $9-29/month, the cost is negligible compared to the flexibility and insights gained.</p> - - <p>Start with QR Master's free plan (3 dynamic codes) to test the technology. Upgrade when you need more codes or advanced features. Future-proof your QR strategy with dynamic codes, and never worry about reprint costs again.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Ready to Create Dynamic QR Codes?</h3> - <p class="text-lg text-gray-700 mb-6">Start with 3 free dynamic QR codes. No credit card required. Full analytics dashboard included.</p> - <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Get Started Free →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a> - Create editable, trackable QR codes</li> - <li><a href="/blog/qr-code-tracking-guide-2025">QR Code Tracking Guide</a> - Learn how to track scans</li> - <li><a href="/blog/bulk-qr-code-generator-excel">Bulk QR Generator from Excel</a> - Generate hundreds at once</li> - <li><a href="/pricing">Pricing Plans</a> - Compare free and paid options</li> - <li><a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">QR Code on Wikipedia</a> - Technical standards (ISO/IEC 18004)</li> - <li><a href="https://meta.wikimedia.org/wiki/QRpedia" target="_blank" rel="noopener">QRpedia on Wikipedia</a> - Real-world dynamic QR example</li> - </ul> - </div>`, - }, - 'bulk-qr-code-generator-excel': { - slug: 'bulk-qr-code-generator-excel', - title: 'How to Generate Bulk QR Codes from Excel: Complete Tutorial 2025', - excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools. Perfect for products, events, inventory.', - date: 'October 16, 2025', - datePublished: '2025-10-16T10:00:00Z', - dateModified: '2025-10-16T10:00:00Z', - readTime: '13 Min', - category: 'Bulk Generation', - image: '/blog/3-hero.png', - imageAlt: 'Multiple QR codes arranged in organized grid pattern', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'Bulk QR code generation from Excel allows you to create hundreds or thousands of QR codes simultaneously by uploading a CSV or Excel file. The file should contain columns for name, URL, and optional metadata. Tools like QR Master Pro can process 1,000+ codes in minutes, saving hours of manual work. Perfect for product labels, event tickets, asset tracking, and marketing campaigns.', - howTo: { - name: 'How to Generate Bulk QR Codes from Excel', - description: 'Step-by-step tutorial for creating multiple QR codes from Excel or CSV files', - totalTime: 'PT10M', - steps: [ - { - name: 'Prepare Your Excel File', - text: 'Create an Excel or CSV file with columns: name, url, description, and tags. Fill in your data with one QR code per row.', - }, - { - name: 'Sign Up for QR Master Business', - text: 'Create a QR Master account and upgrade to Business plan for bulk upload feature (supports up to 500 codes).', - url: 'https://www.qrmaster.net/signup', - }, - { - name: 'Upload Your File', - text: 'Navigate to Create → Bulk Upload and drag-drop your Excel/CSV file. The system will auto-detect columns.', - }, - { - name: 'Map Columns and Customize', - text: 'Verify column mapping is correct. Optionally customize QR design: add logo, set colors, choose frame style.', - }, - { - name: 'Generate and Download', - text: 'Click Generate All. Processing takes 2-4 minutes for 1,000 codes. Download the ZIP file with all QR codes organized by name.', - url: 'https://www.qrmaster.net/bulk-qr-code-generator', - }, - ], - }, - content: `<div class="blog-content"> - <p>Need to create QR codes for 100, 500, or even 1,000 products? Manual generation would take hours—but bulk QR code generation from Excel or CSV files can complete the job in minutes. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented for tracking automotive parts in bulk, and today's batch generation tools continue that efficiency for modern applications.</p> - - <p>This comprehensive guide shows you exactly how to generate bulk QR codes from Excel, including file format requirements, step-by-step tutorials, use cases, and tool comparisons. Perfect for e-commerce, events, inventory management, and marketing campaigns.</p> - - <h2>What is Bulk QR Code Generation?</h2> - - <p>Bulk QR code generation is the process of creating multiple QR codes simultaneously from a data file (Excel or CSV). Instead of manually entering data for each QR code one-by-one, you upload a spreadsheet containing all your data, and the system generates all QR codes automatically.</p> - - <h3>Time Savings Comparison</h3> - <p><strong>Manual Creation:</strong> 2-5 minutes per QR code</p> - <ul> - <li>100 codes = 3-8 hours of repetitive work</li> - <li>500 codes = 16-40 hours (2-5 full work days!)</li> - <li>1,000 codes = 33-83 hours</li> - </ul> - - <p><strong>Bulk Creation from Excel:</strong> 2-3 minutes total</p> - <ul> - <li>100 codes = 2 minutes</li> - <li>500 codes = 2-3 minutes</li> - <li>1,000 codes = 3-4 minutes</li> - </ul> - - <p><strong>Time Saved:</strong> For 500 QR codes, bulk generation saves approximately 16-40 hours of work. That's nearly a full work week of productivity gained.</p> - - <h3>Common Use Cases for Bulk QR Generation</h3> - <ul> - <li><strong>Product Labels:</strong> Generate QR code for each SKU linking to product manual, warranty, or reviews</li> - <li><strong>Event Tickets:</strong> Create unique QR codes for each attendee for check-in and access control</li> - <li><strong>Asset Management:</strong> Track office equipment, IT hardware, or inventory with QR stickers</li> - <li><strong>Marketing Campaigns:</strong> Multiple store locations each get unique QR code for tracking</li> - <li><strong>Restaurant Menus:</strong> Different QR codes for each dish or table</li> - <li><strong>Real Estate:</strong> Unique QR code for each property listing</li> - <li><strong>Business Cards:</strong> Generate personalized QR codes for each team member</li> - </ul> - - <h2>How Bulk QR Generation Works</h2> - - <pre class="bg-gray-100 p-6 rounded-lg my-6 overflow-x-auto"> -Step 1: Prepare Data - ↓ -Excel/CSV File: -Product Name | URL | SKU -Product A | https://manual.com/product-a | 001 -Product B | https://manual.com/product-b | 002 -Product C | https://manual.com/product-c | 003 - -Step 2: Upload to QR Generator - ↓ -Map Columns: -‱ Name → QR Code Title -‱ URL → Destination URL -‱ SKU → File Name - -Step 3: Customize Design (Optional) - ↓ -Apply Branding to ALL Codes: -‱ Upload Logo -‱ Set Brand Colors -‱ Choose Frame Style -‱ Set Image Size - -Step 4: Generate & Download - ↓ -Download ZIP File: -📩 qr-codes.zip - ├─ product-001.png - ├─ product-002.png - ├─ product-003.png - └─ ... (all codes) - </pre> - - <h3>System Requirements</h3> - <ul> - <li><strong>File Format:</strong> Excel (.xlsx) or CSV (.csv)</li> - <li><strong>Minimum Columns:</strong> Name and URL (required)</li> - <li><strong>Optional Columns:</strong> Description, Tags, Category, Custom Fields</li> - <li><strong>Max File Size:</strong> Usually 10-50MB depending on service</li> - <li><strong>Max Rows:</strong> 1,000-10,000 depending on service tier</li> - </ul> - - <h3>Output Format</h3> - <ul> - <li><strong>File Type:</strong> ZIP archive containing individual QR code images</li> - <li><strong>Image Format:</strong> PNG (most common), SVG, or PDF</li> - <li><strong>Resolution:</strong> 200x200px (web), 500x500px (print), 1000x1000px (high-res)</li> - <li><strong>File Naming:</strong> Based on your chosen column (SKU, name, or custom)</li> - <li><strong>Organization:</strong> All codes in one folder, optionally organized by category</li> - </ul> - - <h2>Excel File Format & Template</h2> - - <h3>Required Columns</h3> - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Column Name</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Description</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Required</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Example</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">name</td> - <td class="border border-gray-300 px-6 py-4">QR code title/label</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">Yes</td> - <td class="border border-gray-300 px-6 py-4">Summer Promo Flyer</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">url</td> - <td class="border border-gray-300 px-6 py-4">Destination URL</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">Yes</td> - <td class="border border-gray-300 px-6 py-4">https://example.com/sale</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">description</td> - <td class="border border-gray-300 px-6 py-4">Optional notes</td> - <td class="border border-gray-300 px-6 py-4 text-gray-600">No</td> - <td class="border border-gray-300 px-6 py-4">50% off summer sale</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">tags</td> - <td class="border border-gray-300 px-6 py-4">Categories (comma-separated)</td> - <td class="border border-gray-300 px-6 py-4 text-gray-600">No</td> - <td class="border border-gray-300 px-6 py-4">marketing, summer, 2025</td> - </tr> - </tbody> - </table> - </div> - - <h3>Example Excel File Content</h3> - <pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto"> -name,url,description,tags -Product A Manual,https://manuals.com/product-a,User manual for Product A,manuals electronics -Product B Warranty,https://warranty.com/product-b,Warranty registration,warranty electronics -Store Location NYC,https://maps.com/store-nyc,NYC store directions,locations stores -Store Location LA,https://maps.com/store-la,LA store directions,locations stores -Event Ticket 001,https://checkin.com/verify/001,VIP ticket,events tickets -Event Ticket 002,https://checkin.com/verify/002,General admission,events tickets - </pre> - - <h3>Best Practices for File Preparation</h3> - - <h4>1. Clean Your Data</h4> - <ul> - <li><strong>Remove empty rows:</strong> Delete any rows with missing required fields</li> - <li><strong>Validate URLs:</strong> Ensure all URLs start with https:// or http://</li> - <li><strong>No special characters in filenames:</strong> Avoid / \ : * ? " < > | in name column</li> - <li><strong>Consistent naming:</strong> Use a standardized naming scheme (e.g., PROD-001, PROD-002)</li> - <li><strong>Check for duplicates:</strong> Remove duplicate URLs or names if not intended</li> - </ul> - - <h4>2. Test with Small Batch First</h4> - <ul> - <li>Upload only 5-10 rows initially</li> - <li>Verify output is correct (check 2-3 QR codes)</li> - <li>Confirm file naming and organization matches expectations</li> - <li>Then upload your full dataset</li> - </ul> - - <h4>3. URL Formatting</h4> - <ul> - <li><strong>Include protocol:</strong> Always use <code>https://example.com</code> not <code>example.com</code></li> - <li><strong>Test all URLs:</strong> Click each link to verify it works</li> - <li><strong>Use URL shorteners if needed:</strong> Shorter URLs = simpler QR codes</li> - <li><strong>Avoid special characters:</strong> URL-encode spaces and special characters</li> - </ul> - - <h4>4. Smart File Naming</h4> - <ul> - <li>Use SKU or product ID in name column: "PROD-001", "PROD-002"</li> - <li>Keeps downloaded files organized and easy to identify</li> - <li>Matches physical inventory labels</li> - <li>Enables easy search and sorting</li> - </ul> - - <h4>5. File Size Management</h4> - <ul> - <li><strong>Keep under 10MB:</strong> Most services have file size limits</li> - <li><strong>Split large datasets:</strong> Divide 5,000 rows into 5 files of 1,000 each</li> - <li><strong>Typical row size:</strong> 1,000 rows ≈ 50-100KB</li> - <li><strong>Remove unnecessary columns:</strong> Only include columns you need</li> - </ul> - - <div class="my-8"> - <img src="/blog/3-body.png" alt="Person working at desk with laptop and QR code materials" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>Step-by-Step Tutorial with QR Master</h2> - - <h3>Step 1: Prepare Your Excel File</h3> - <ol> - <li>Open Excel, Google Sheets, or any spreadsheet app</li> - <li>Create columns: <code>name</code>, <code>url</code>, <code>description</code>, <code>tags</code></li> - <li>Fill in your data (one QR code per row)</li> - <li>Example: - <pre class="bg-gray-100 p-4 rounded-lg my-4"> -name | url | tags -Product A | https://shop.com/product-a | electronics, sale -Product B | https://shop.com/product-b | electronics -Event Ticket 1 | https://event.com/ticket/1 | events, tickets - </pre> - </li> - <li>Save as <code>.xlsx</code> or export as <code>.csv</code></li> - </ol> - - <h3>Step 2: Sign Up for QR Master</h3> - <ol> - <li>Go to <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li> - <li>Create free account (email + password)</li> - <li>Verify your email</li> - <li><strong>Free plan:</strong> Up to 3 dynamic QR codes (no bulk upload)</li> - <li><strong>Business plan:</strong> $29/month, up to 500 codes, bulk upload feature ✅</li> - </ol> - - <h3>Step 3: Navigate to Bulk Upload</h3> - <ol> - <li>Log into your QR Master dashboard</li> - <li>Click <strong>"Create QR Code"</strong> button</li> - <li>Select <strong>"Bulk Upload"</strong> tab</li> - <li>Choose <strong>"Upload Excel/CSV"</strong></li> - <li>Or drag and drop your file directly</li> - </ol> - - <h3>Step 4: Map Your Columns</h3> - <ol> - <li>System auto-detects column names</li> - <li>Verify mapping is correct: - <ul> - <li><code>name</code> → QR Code Title</li> - <li><code>url</code> → Destination URL</li> - <li><code>description</code> → Description</li> - <li><code>tags</code> → Tags</li> - </ul> - </li> - <li>Preview shows first 5 rows</li> - <li>Check data looks correct</li> - <li>Click <strong>"Looks Good"</strong> to proceed</li> - </ol> - - <h3>Step 5: Customize Design (Optional)</h3> - <p>Apply branding to ALL QR codes simultaneously:</p> - - <h4>Upload Logo</h4> - <ul> - <li>Click "Upload Logo"</li> - <li>Select PNG or SVG (max 1MB)</li> - <li>Logo appears in center of all QR codes</li> - <li>Recommended: Square logo, transparent background</li> - </ul> - - <h4>Set Colors</h4> - <ul> - <li><strong>Foreground:</strong> QR code pattern color (default: #000000 black)</li> - <li><strong>Background:</strong> QR code background (default: #FFFFFF white)</li> - <li><strong>Use brand colors:</strong> e.g., #FF6B6B for foreground, #FFFFFF for background</li> - <li><strong>Ensure contrast:</strong> Dark foreground + light background for scannability</li> - </ul> - - <h4>Choose Frame Style</h4> - <ul> - <li><strong>No frame:</strong> Plain QR code only</li> - <li><strong>Square frame:</strong> Professional border</li> - <li><strong>Rounded frame:</strong> Modern, friendly look</li> - <li><strong>With text:</strong> Add "Scan Me" or custom CTA text</li> - </ul> - - <h4>Set Image Size</h4> - <ul> - <li><strong>200x200px:</strong> Web use, social media</li> - <li><strong>500x500px:</strong> Standard print (business cards, flyers)</li> - <li><strong>1000x1000px:</strong> High-resolution print (posters, banners)</li> - <li><strong>2000x2000px:</strong> Billboard, large-format print</li> - </ul> - - <h3>Step 6: Generate QR Codes</h3> - <ol> - <li>Click <strong>"Generate All"</strong> button</li> - <li>System begins processing: - <ul> - <li>100 codes ≈ 30 seconds</li> - <li>500 codes ≈ 2 minutes</li> - <li>1,000 codes ≈ 4 minutes</li> - </ul> - </li> - <li>Progress bar shows real-time status</li> - <li>Email notification when complete (for large batches)</li> - <li>Do not close browser window while processing</li> - </ol> - - <h3>Step 7: Download & Use</h3> - <ol> - <li>Click <strong>"Download ZIP"</strong> button</li> - <li>ZIP file downloads to your computer</li> - <li>Extract the archive: - <ul> - <li>Windows: Right-click → Extract All</li> - <li>Mac: Double-click ZIP file</li> - </ul> - </li> - <li>Files are named using your <code>name</code> column</li> - <li>Example: <code>product-001.png</code>, <code>product-002.png</code></li> - <li>Organized and ready to use immediately</li> - </ol> - - <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> - <h3 class="text-xl font-semibold mb-2 text-gray-900">Pro Tip: CSV Export from Google Sheets</h3> - <p class="text-gray-800">Google Sheets users: File → Download → Comma-separated values (.csv). This format works perfectly with all QR code generators and is often smaller than Excel files.</p> - </div> - - <h2>Use Cases & Examples</h2> - - <h3>1. E-Commerce Product Labels</h3> - <p><strong>Scenario:</strong> Online store has 500 products, each needs QR code linking to:</p> - <ul> - <li>Product manual PDF</li> - <li>Warranty registration page</li> - <li>Customer support contact</li> - </ul> - - <p><strong>Excel Setup:</strong></p> - <pre class="bg-gray-100 p-4 rounded-lg my-4"> -name,url -SKU-001,https://manual.com/sku-001 -SKU-002,https://manual.com/sku-002 -SKU-003,https://manual.com/sku-003 -... - </pre> - - <p><strong>Result:</strong></p> - <ul> - <li>500 QR codes generated in 2 minutes</li> - <li>Print on product label stickers</li> - <li>Stick on packaging before shipping</li> - <li>Track which products get most support requests via scan analytics</li> - </ul> - - <p><strong>Time Saved:</strong> 500 codes × 3 min/code = 25 hours saved!</p> - - <h3>2. Event Tickets (1,000 Attendees)</h3> - <p><strong>Scenario:</strong> Conference with 1,000 attendees, each needs unique QR code for:</p> - <ul> - <li>Check-in at venue</li> - <li>Session access verification</li> - <li>Prevent duplicate entries</li> - </ul> - - <p><strong>Excel Setup:</strong></p> - <pre class="bg-gray-100 p-4 rounded-lg my-4"> -name,url,description -Ticket-001,https://checkin.com/verify/001,John Doe - VIP -Ticket-002,https://checkin.com/verify/002,Jane Smith - General -Ticket-003,https://checkin.com/verify/003,Bob Johnson - Speaker -... - </pre> - - <p><strong>Result:</strong></p> - <ul> - <li>Unique QR per ticket (prevents sharing)</li> - <li>Real-time check-in tracking</li> - <li>Instant attendance reports</li> - <li>Session-specific access control</li> - </ul> - - <div class="my-8"> - <img src="https://images.unsplash.com/photo-1505373877841-8d25f7d46678?w=800&q=80" alt="Event tickets with unique QR codes" class="rounded-lg shadow-lg w-full" /> - </div> - - <h3>3. Asset Management (200 Office Items)</h3> - <p><strong>Scenario:</strong> IT department needs to track office equipment:</p> - <ul> - <li>Laptops</li> - <li>Monitors</li> - <li>Desks and chairs</li> - <li>Printers</li> - </ul> - - <p><strong>Excel Setup:</strong></p> - <pre class="bg-gray-100 p-4 rounded-lg my-4"> -name,url,description -LAPTOP-001,https://assets.com/laptop-001,Dell Latitude 5420 -LAPTOP-002,https://assets.com/laptop-002,MacBook Pro 14" -DESK-001,https://assets.com/desk-001,Standing Desk - Office 3A -PRINTER-001,https://assets.com/printer-001,HP LaserJet Pro -... - </pre> - - <p><strong>Result:</strong></p> - <ul> - <li>QR code sticker on each item</li> - <li>Scan to view: Current owner, purchase date, warranty, maintenance history</li> - <li>Update info dynamically (no sticker replacement needed)</li> - <li>Easy inventory audits</li> - </ul> - - <h3>4. Multi-Location Marketing (50 Stores)</h3> - <p><strong>Scenario:</strong> Retail chain with 50 locations, each gets unique QR code for:</p> - <ul> - <li>Local promotions</li> - <li>Store-specific tracking</li> - <li>Regional offers</li> - </ul> - - <p><strong>Excel Setup:</strong></p> - <pre class="bg-gray-100 p-4 rounded-lg my-4"> -name,url,tags -NYC-Store,https://promo.com?location=nyc,new-york retail -LA-Store,https://promo.com?location=la,california retail -Chicago-Store,https://promo.com?location=chicago,illinois retail -... - </pre> - - <p><strong>Result:</strong></p> - <ul> - <li>Track which stores drive most QR scans</li> - <li>Different promotions per location</li> - <li>Measure local campaign ROI</li> - <li>Optimize regional marketing spend</li> - </ul> - - <h2>Free vs Paid Bulk QR Tools</h2> - - <h3>Free Tools</h3> - - <h4>1. QR Master Free</h4> - <ul> - <li><strong>Limit:</strong> 3 dynamic codes (no bulk upload feature)</li> - <li><strong>Best for:</strong> Testing the platform before upgrading</li> - <li><strong>Upgrade Path:</strong> $29/mo Business plan for bulk + 500 codes</li> - </ul> - - <h4>2. QuickChart (API-Based)</h4> - <ul> - <li><strong>Type:</strong> Open-source API</li> - <li><strong>Free:</strong> Yes, unlimited</li> - <li><strong>Bulk:</strong> Requires coding (Python, JavaScript, etc.)</li> - <li><strong>Best for:</strong> Developers comfortable with APIs</li> - </ul> - - <h4>3. Google Sheets + Apps Script</h4> - <ul> - <li><strong>Limit:</strong> 100 codes per execution (can run multiple times)</li> - <li><strong>Free:</strong> Yes</li> - <li><strong>Bulk:</strong> Requires Google Apps Script knowledge</li> - <li><strong>Complexity:</strong> Moderate technical skill required</li> - </ul> - - <h3>Paid Tools (Recommended for Business)</h3> - - <div class="overflow-x-auto my-8"> - <table class="min-w-full border-collapse border border-gray-300"> - <thead class="bg-gray-100"> - <tr> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Tool</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Price</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Max Codes</th> - <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Bulk Upload</th> - </tr> - </thead> - <tbody> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Pro</td> - <td class="border border-gray-300 px-6 py-4">$9/mo</td> - <td class="border border-gray-300 px-6 py-4">50 codes</td> - <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Business</td> - <td class="border border-gray-300 px-6 py-4">$29/mo</td> - <td class="border border-gray-300 px-6 py-4">500 codes</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Excel/CSV</td> - </tr> - <tr class="bg-white"> - <td class="border border-gray-300 px-6 py-4 font-medium">QR Code Generator</td> - <td class="border border-gray-300 px-6 py-4">$50/mo</td> - <td class="border border-gray-300 px-6 py-4">Unlimited</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Excel/CSV</td> - </tr> - <tr class="bg-gray-50"> - <td class="border border-gray-300 px-6 py-4 font-medium">Beaconstac</td> - <td class="border border-gray-300 px-6 py-4">$99/mo</td> - <td class="border border-gray-300 px-6 py-4">500 codes</td> - <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Excel/CSV</td> - </tr> - </tbody> - </table> - </div> - - <p><strong>Our Recommendation:</strong></p> - <ul> - <li><strong>For 1-50 codes:</strong> Manual creation or QR Master Pro</li> - <li><strong>For 50-500 codes:</strong> QR Master Business at $29/mo (best value)</li> - <li><strong>For 500+ codes:</strong> QR Master Business or enterprise custom quote</li> - <li><strong>For developers:</strong> QuickChart API (free, unlimited, requires coding)</li> - </ul> - - <h2>Advanced Tips & Tricks</h2> - - <h3>1. Use Dynamic QR Codes for Bulk Generation</h3> - <p>Always use dynamic QR codes for bulk generation (even though they cost more). Why?</p> - <ul> - <li><strong>Edit any URL later:</strong> If Product A manual URL changes, update it without reprinting 10,000 labels</li> - <li><strong>Track individual code performance:</strong> See which products get most scans</li> - <li><strong>Future-proof:</strong> Protect your investment in printed materials</li> - </ul> - - <h3>2. Organize with Tags and Categories</h3> - <p>Use the <code>tags</code> column strategically:</p> - <ul> - <li><strong>Product category:</strong> "electronics", "clothing", "food"</li> - <li><strong>Campaign:</strong> "summer-2025", "black-friday"</li> - <li><strong>Location:</strong> "store-nyc", "warehouse-la"</li> - <li><strong>Status:</strong> "active", "archived", "seasonal"</li> - </ul> - <p>This enables bulk filtering and management in your dashboard later.</p> - - <h3>3. Test Scannability Before Mass Printing</h3> - <p>Before printing 10,000 QR codes:</p> - <ol> - <li>Print 5-10 test codes on the actual material (paper, vinyl, etc.)</li> - <li>Scan from multiple devices (iOS, Android, different scanner apps)</li> - <li>Test various distances: 6 inches, 12 inches, 24 inches</li> - <li>Check different lighting: bright sun, indoor, dim light</li> - <li>Verify destination URLs are correct</li> - </ol> - - <h3>4. Naming Convention Best Practices</h3> - <p>Use smart naming in your <code>name</code> column:</p> - <ul> - <li><strong>Sequential:</strong> PROD-001, PROD-002, PROD-003</li> - <li><strong>Hierarchical:</strong> NYC-STORE-001, NYC-STORE-002, LA-STORE-001</li> - <li><strong>Descriptive:</strong> Include product name: "PROD-001-Laptop-Dell-5420"</li> - </ul> - <p>Downloaded files will have these names, making organization easy.</p> - - <h3>5. Automate with API (Advanced)</h3> - <p>For recurring bulk generation needs, use QR Master's API:</p> - <pre class="bg-gray-100 p-4 rounded-lg my-4"> -// Example: Node.js API call -const response = await fetch('https://api.qrmaster.net/v1/bulk', { - method: 'POST', - headers: { - 'Authorization': 'Bearer YOUR_API_KEY', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - codes: [ - { name: 'Product A', url: 'https://example.com/a' }, - { name: 'Product B', url: 'https://example.com/b' } - ] - }) -}); - </pre> - <p>Perfect for integrating with inventory systems, e-commerce platforms, or automated workflows.</p> - - <h2>Common Mistakes to Avoid</h2> - - <h3>Mistake 1: Using Static QR Codes for Bulk</h3> - <p><strong>Problem:</strong> Generate 5,000 static QR codes, then URL structure changes—all codes are now useless.</p> - <p><strong>Solution:</strong> Always use dynamic QR codes for bulk generation. The small monthly cost ($29) is insignificant compared to reprint costs.</p> - - <h3>Mistake 2: Not Testing Before Mass Printing</h3> - <p><strong>Problem:</strong> Print 10,000 labels, discover QR codes are too small to scan reliably.</p> - <p><strong>Solution:</strong> Print 10 test labels, scan with multiple devices in various conditions before committing to full print run.</p> - - <h3>Mistake 3: Poor File Organization</h3> - <p><strong>Problem:</strong> Download 500 QR codes all named "qr-1.png", "qr-2.png"—impossible to identify which is which.</p> - <p><strong>Solution:</strong> Use descriptive names in your Excel <code>name</code> column: "SKU-001-ProductA", "SKU-002-ProductB".</p> - - <h3>Mistake 4: Forgetting URL Protocols</h3> - <p><strong>Problem:</strong> URLs like <code>example.com</code> (missing https://) cause QR scanners to fail or treat as plain text.</p> - <p><strong>Solution:</strong> Always include full URL: <code>https://example.com</code>. Double-check all URLs before upload.</p> - - <h3>Mistake 5: Exceeding Service Limits</h3> - <p><strong>Problem:</strong> Upload 1,000 codes on a plan that supports only 500.</p> - <p><strong>Solution:</strong> Check your plan limits. Split large batches or upgrade plan before uploading.</p> - - <h2>Conclusion</h2> - - <p>Bulk QR code generation from Excel transforms hours of tedious manual work into minutes of automated efficiency. For any project requiring more than 10-20 QR codes, bulk generation is the only practical approach.</p> - - <p><strong>Key Takeaways:</strong></p> - <ul> - <li>Excel/CSV format: <code>name</code>, <code>url</code>, <code>description</code>, <code>tags</code></li> - <li>Always use <strong>dynamic QR codes</strong> for bulk (editable + trackable)</li> - <li>Test with 5-10 codes before mass printing</li> - <li>QR Master Business ($29/mo) supports up to 500 codes with bulk upload</li> - <li>Time saved: 16-40 hours for 500 codes</li> - </ul> - - <p>Whether you're managing product labels, event tickets, asset tracking, or marketing campaigns, bulk QR generation is an essential productivity tool. Start with a small test batch, optimize your process, then scale to thousands of codes with confidence.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Ready to Generate Bulk QR Codes?</h3> - <p class="text-lg text-gray-700 mb-6">Start with QR Master Business plan: 500 codes, bulk Excel/CSV upload, full analytics. 14-day money-back guarantee.</p> - <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Start Bulk Generation →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/bulk-qr-code-generator">Bulk QR Code Generator</a> - Create hundreds of codes from Excel</li> - <li><a href="/blog/qr-code-tracking-guide-2025">QR Code Tracking Guide</a> - Track every scan</li> - <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a> - Understand the difference</li> - <li><a href="/pricing">Pricing Plans</a> - Compare plans and features</li> - <li><a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">QR Code on Wikipedia</a> - Technical standards (ISO/IEC 18004)</li> - </ul> - </div>`, - }, - - // ============ NEW BLOG POSTS ============ - - 'qr-code-restaurant-menu': { - slug: 'qr-code-restaurant-menu', - title: 'How to Create a QR Code for Restaurant Menu: Complete 2025 Guide', - excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.', - date: 'January 5, 2026', - datePublished: '2026-01-05T09:00:00Z', - dateModified: '2026-01-05T09:00:00Z', - readTime: '12 Min', - category: 'Restaurant', - image: '/blog/restaurant-qr-menu.png', - imageAlt: 'Restaurant table with QR code menu card and smartphone scanning', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'To create a QR code for your restaurant menu, use a dynamic QR code generator like QR Master. Upload your menu PDF or link to your online menu, customize the QR code design, print it on table tents or cards, and track scans to understand customer engagement.', - howTo: { - name: 'How to Create a Restaurant Menu QR Code', - description: 'Complete guide to setting up touchless digital menus with QR codes', - totalTime: 'PT15M', - steps: [ - { - name: 'Prepare Your Digital Menu', - text: 'Create a mobile-friendly menu using PDF, Google Docs, or a dedicated menu platform. Ensure it loads quickly on smartphones.', - }, - { - name: 'Generate a Dynamic QR Code', - text: 'Use QR Master to create a dynamic QR code. This allows you to update your menu URL anytime without reprinting codes.', - url: 'https://www.qrmaster.net/create', - }, - { - name: 'Customize Your QR Code Design', - text: 'Add your restaurant logo, match brand colors, and ensure high contrast for easy scanning.', - }, - { - name: 'Print and Place Strategically', - text: 'Print QR codes on table tents, coasters, or wall-mounted displays. Minimum size: 2x2 inches for table scanning.', - }, - { - name: 'Track and Optimize', - text: 'Monitor scan analytics in your QR Master dashboard to understand peak times and popular menu items.', - url: 'https://www.qrmaster.net/analytics', - }, - ], - }, - content: `<div class="blog-content"> - <h2>Why Restaurants Need QR Code Menus in 2025</h2> - <p>Digital QR code menus have evolved from a pandemic necessity to a restaurant industry standard. In 2025, over 60% of diners prefer scanning a QR code over handling physical menus. For restaurant owners, QR menus offer significant benefits: reduced printing costs, instant menu updates, and valuable customer analytics.</p> - <p>Whether you run a fine dining establishment, casual cafĂ©, or food truck, implementing a <strong>restaurant menu QR code</strong> system can streamline operations and enhance the guest experience.</p> - - <h2>Step 1: Prepare Your Digital Menu</h2> - <h3>Menu Format Options</h3> - <p>Choose the right format for your digital menu:</p> - <ul> - <li><strong>PDF Menu:</strong> Simple and universal. Upload your existing menu design as a PDF for instant access.</li> - <li><strong>Website/Landing Page:</strong> Create a dedicated menu page on your website with images and descriptions.</li> - <li><strong>Menu Platform:</strong> Use services like Square, Toast, or dedicated menu apps for interactive features.</li> - <li><strong>Google Doc:</strong> Free option that allows real-time updates shared via link.</li> - </ul> - - <h3>Mobile Optimization Tips</h3> - <p>Your digital menu must be mobile-friendly since 95% of scans come from smartphones:</p> - <ul> - <li>Use readable font sizes (minimum 16px)</li> - <li>Ensure fast load times (under 3 seconds)</li> - <li>Make buttons and links thumb-friendly</li> - <li>Test on both iOS and Android devices</li> - </ul> - - <h2>Step 2: Create Your QR Code with QR Master</h2> - - <div class="my-8"> - <img src="/blog/restaurant-qr-body.png" alt="Customer scanning QR code menu at restaurant" class="rounded-lg shadow-lg w-full" /> - </div> - - <p>Using a <a href="/dynamic-qr-code-generator">dynamic QR code generator</a> is essential for restaurants. Unlike static codes, dynamic QR codes let you:</p> - <ul> - <li><strong>Update your menu URL anytime</strong> without reprinting QR codes</li> - <li><strong>Track scan analytics</strong> to understand customer behavior</li> - <li><strong>A/B test different landing pages</strong> for seasonal menus</li> - <li><strong>Schedule changes</strong> for lunch vs. dinner menus</li> - </ul> - - <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> - <h3 class="text-xl font-semibold mb-2">Pro Tip: Use Dynamic QR Codes</h3> - <p>Static QR codes encode the URL directly—if your menu URL changes, you need new codes. Dynamic codes redirect through our servers, allowing unlimited URL updates. <a href="/blog/dynamic-vs-static-qr-codes">Learn the difference</a>.</p> - </div> - - <h2>Step 3: Customize Your Restaurant QR Code</h2> - <p>Branding matters. A generic black-and-white QR code looks out of place in a well-designed restaurant. Customize your code to match your brand:</p> - <ul> - <li><strong>Add your logo:</strong> Place your restaurant logo in the center of the QR code</li> - <li><strong>Match brand colors:</strong> Use your brand's color palette for foreground and background</li> - <li><strong>Choose corner styles:</strong> Rounded corners for casual vibes, square for modern/minimal</li> - <li><strong>Maintain contrast:</strong> Ensure minimum 3:1 contrast ratio for reliable scanning</li> - </ul> - - <h2>Step 4: Print and Placement Best Practices</h2> - <h3>Optimal QR Code Sizes for Restaurants</h3> - <table class="w-full border-collapse my-6"> - <thead> - <tr class="bg-gray-100"> - <th class="border p-3 text-left">Placement</th> - <th class="border p-3 text-left">Minimum Size</th> - <th class="border p-3 text-left">Recommended Size</th> - </tr> - </thead> - <tbody> - <tr><td class="border p-3">Table tent</td><td class="border p-3">2" x 2"</td><td class="border p-3">2.5" x 2.5"</td></tr> - <tr><td class="border p-3">Coaster</td><td class="border p-3">1.5" x 1.5"</td><td class="border p-3">2" x 2"</td></tr> - <tr><td class="border p-3">Wall poster</td><td class="border p-3">4" x 4"</td><td class="border p-3">6" x 6"</td></tr> - <tr><td class="border p-3">Window decal</td><td class="border p-3">3" x 3"</td><td class="border p-3">4" x 4"</td></tr> - </tbody> - </table> - - <p>Learn more about <a href="/blog/qr-code-print-size-guide">optimal QR code print sizes</a> for various materials.</p> - - <h3>Strategic Placement Locations</h3> - <ul> - <li><strong>On every table:</strong> Table tents or built-in holders</li> - <li><strong>At the entrance:</strong> Allow guests to browse while waiting</li> - <li><strong>On takeout packaging:</strong> Link to your full menu or loyalty program</li> - <li><strong>At the bar:</strong> Separate drink menu access</li> - </ul> - - <h2>Step 5: Track and Analyze Menu Scans</h2> - <p>With <a href="/qr-code-tracking">QR code tracking</a>, you gain valuable insights:</p> - <ul> - <li><strong>Peak scanning times:</strong> Understand when guests are viewing your menu</li> - <li><strong>Device types:</strong> Optimize for the most common devices</li> - <li><strong>Scan locations:</strong> See which tables or areas have most engagement</li> - <li><strong>Repeat scans:</strong> Identify returning customers</li> - </ul> - - <h2>Common Mistakes to Avoid</h2> - <ul> - <li>❌ Using static QR codes (can't update menu URL)</li> - <li>❌ Too small print size (under 1.5 inches)</li> - <li>❌ Poor lighting near QR code placement</li> - <li>❌ Linking to non-mobile-friendly PDFs</li> - <li>❌ No call-to-action text near the code</li> - </ul> - - <h2>Conclusion</h2> - <p>Creating a QR code for your restaurant menu is straightforward with the right approach. Use dynamic QR codes for flexibility, customize to match your brand, print at appropriate sizes, and track analytics to continuously improve the guest experience.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Create Your Restaurant Menu QR Code</h3> - <p class="text-lg text-gray-700 mb-6">Start free with QR Master—no credit card required. Update your menu anytime and track every scan.</p> - <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create Menu QR Free →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a></li> - <li><a href="/blog/qr-code-print-size-guide">QR Code Print Size Guide</a></li> - <li><a href="/blog/qr-code-analytics">QR Code Analytics Guide</a></li> - <li><a href="/pricing">Pricing Plans</a></li> - </ul> - </div>`, - }, - - 'vcard-qr-code-generator': { - slug: 'vcard-qr-code-generator', - title: 'Free vCard QR Code Generator: Digital Business Cards Made Easy', - excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.', - date: 'January 5, 2026', - datePublished: '2026-01-05T10:00:00Z', - dateModified: '2026-01-05T10:00:00Z', - readTime: '10 Min', - category: 'Business Cards', - image: '/blog/vcard-qr-code.png', - imageAlt: 'Professional business card with vCard QR code being scanned by smartphone', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'A vCard QR code contains your contact information in a standardized format. When scanned, it allows the recipient to save your name, phone, email, company, and social links directly to their phone contacts with one tap.', - howTo: { - name: 'How to Create a vCard QR Code', - description: 'Step-by-step guide to creating digital business card QR codes', - totalTime: 'PT5M', - steps: [ - { - name: 'Enter Your Contact Information', - text: 'Fill in your name, phone number, email, company, job title, and website URL.', - }, - { - name: 'Add Social Media Links', - text: 'Include LinkedIn, Twitter, or other professional networks you want to share.', - }, - { - name: 'Customize the QR Code Design', - text: 'Match your personal or company branding with custom colors and logo.', - }, - { - name: 'Download and Print', - text: 'Export as SVG or high-resolution PNG for business cards, email signatures, or presentations.', - }, - ], - }, - content: `<div class="blog-content"> - <h2>What is a vCard QR Code?</h2> - <p>A vCard (Virtual Contact File) QR code contains your contact information in a standardized format (.vcf). When someone scans it with their smartphone camera, they can instantly save your details to their contacts—no typing required.</p> - <p>This technology has revolutionized professional networking. Instead of handing out paper business cards that often get lost, a <strong>vCard QR code</strong> ensures your contact information is digitally saved and accessible.</p> - - <h2>Why Use a Digital Business Card QR Code?</h2> - <ul> - <li><strong>Instant Saving:</strong> Recipients add your contact with one tap</li> - <li><strong>Always Up-to-Date:</strong> With dynamic vCards, update your info without new cards</li> - <li><strong>Eco-Friendly:</strong> Reduce paper waste from traditional business cards</li> - <li><strong>Track Engagement:</strong> See who scanned and when</li> - <li><strong>Rich Information:</strong> Include social links, profile photos, and more</li> - </ul> - - <div class="my-8"> - <img src="/blog/vcard-qr-body.png" alt="Business professionals exchanging digital business cards" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>Information You Can Include in a vCard</h2> - <p>A comprehensive vCard QR code can contain:</p> - <ul> - <li><strong>Personal Info:</strong> First name, last name, prefix, suffix</li> - <li><strong>Contact Details:</strong> Mobile, work, and home phone numbers</li> - <li><strong>Email Addresses:</strong> Personal and work email</li> - <li><strong>Company Info:</strong> Company name, job title, department</li> - <li><strong>Address:</strong> Street, city, state, country, postal code</li> - <li><strong>Website:</strong> Personal or company URL</li> - <li><strong>Social Media:</strong> LinkedIn, Twitter, Instagram, Facebook</li> - <li><strong>Profile Photo:</strong> Small image encoded in the vCard</li> - <li><strong>Notes:</strong> Brief description or meeting context</li> - </ul> - - <h2>Static vs Dynamic vCard QR Codes</h2> - <table class="w-full border-collapse my-6"> - <thead> - <tr class="bg-gray-100"> - <th class="border p-3 text-left">Feature</th> - <th class="border p-3 text-left">Static vCard</th> - <th class="border p-3 text-left">Dynamic vCard</th> - </tr> - </thead> - <tbody> - <tr><td class="border p-3">Edit after printing</td><td class="border p-3">❌ No</td><td class="border p-3">✅ Yes</td></tr> - <tr><td class="border p-3">Scan tracking</td><td class="border p-3">❌ No</td><td class="border p-3">✅ Yes</td></tr> - <tr><td class="border p-3">QR code size</td><td class="border p-3">Larger (more data)</td><td class="border p-3">Smaller (redirect URL)</td></tr> - <tr><td class="border p-3">Requires account</td><td class="border p-3">No</td><td class="border p-3">Yes (free)</td></tr> - <tr><td class="border p-3">Works offline</td><td class="border p-3">✅ Yes</td><td class="border p-3">Needs internet</td></tr> - </tbody> - </table> - - <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> - <h3 class="text-xl font-semibold mb-2">Recommendation: Use Dynamic vCards</h3> - <p>If you change jobs, phone numbers, or roles, dynamic vCard QR codes let you update without reprinting business cards. Learn more about <a href="/blog/dynamic-vs-static-qr-codes">dynamic vs static QR codes</a>.</p> - </div> - - <h2>How to Create a vCard QR Code</h2> - <h3>Step 1: Choose Your QR Code Type</h3> - <p>Go to the <a href="/create">QR Master generator</a> and select "Contact Card" or vCard type. Choose between static (data embedded) or dynamic (editable, trackable).</p> - - <h3>Step 2: Enter Your Information</h3> - <p>Fill in the contact form with your details. Required fields typically include:</p> - <ul> - <li>Full name</li> - <li>Primary phone number</li> - <li>Email address</li> - </ul> - <p>Optional but recommended: company name, job title, LinkedIn URL, and website.</p> - - <h3>Step 3: Customize Design</h3> - <p>Make your vCard QR code professional:</p> - <ul> - <li>Add your company logo or headshot</li> - <li>Use brand colors</li> - <li>Ensure good contrast for scanning</li> - </ul> - - <h3>Step 4: Download and Deploy</h3> - <p>Export your QR code in the right format:</p> - <ul> - <li><strong>SVG:</strong> Best for print (scalable, sharp at any size)</li> - <li><strong>PNG (300 DPI):</strong> Good for digital and print</li> - </ul> - - <h2>Where to Use Your vCard QR Code</h2> - <ul> - <li><strong>Business Cards:</strong> Replace or supplement traditional cards</li> - <li><strong>Email Signatures:</strong> Let recipients save your contact instantly</li> - <li><strong>LinkedIn Profile:</strong> Add to your banner or featured section</li> - <li><strong>Conference Badges:</strong> Perfect for networking events</li> - <li><strong>Presentations:</strong> Share contact at the end of talks</li> - <li><strong>Resume/CV:</strong> Modern touch for job applications</li> - </ul> - - <h2>Best Practices for Professional vCards</h2> - <ul> - <li>✅ Keep information current and accurate</li> - <li>✅ Use a professional email address (not personal Gmail)</li> - <li>✅ Include your LinkedIn profile</li> - <li>✅ Test scan before printing in bulk</li> - <li>✅ Use dynamic codes if info may change</li> - <li>❌ Don't overload with too many social links</li> - <li>❌ Avoid personal home addresses</li> - </ul> - - <h2>Conclusion</h2> - <p>vCard QR codes are essential tools for modern professionals. They ensure your contact information is always accessible, up-to-date, and easy to save. Whether you're networking at conferences, meeting clients, or job hunting, a digital business card QR code makes a lasting impression.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Create Your Digital Business Card</h3> - <p class="text-lg text-gray-700 mb-6">Generate a free vCard QR code in seconds. Update anytime, track scans, and share professionally.</p> - <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create vCard QR Free →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/create">QR Code Generator</a></li> - <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a></li> - <li><a href="/blog/qr-code-print-size-guide">QR Code Print Size Guide</a></li> - </ul> - </div>`, - }, - - 'qr-code-small-business': { - slug: 'qr-code-small-business', - title: 'Best QR Code Generator for Small Business: 2025 Complete Guide', - excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.', - date: 'January 5, 2026', - datePublished: '2026-01-05T11:00:00Z', - dateModified: '2026-01-05T11:00:00Z', - readTime: '14 Min', - category: 'Business', - image: '/blog/small-business-qr.png', - imageAlt: 'Small business owner using QR codes for customer engagement', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'The best QR code generator for small business offers dynamic codes with tracking, custom branding, affordable pricing, and easy management. QR Master provides free static codes, 3 free dynamic codes, and Pro plans starting at €9/month for growing businesses.', - content: `<div class="blog-content"> - <h2>Why Small Businesses Need QR Codes</h2> - <p>QR codes have become essential tools for small businesses looking to bridge the gap between physical and digital experiences. From contactless payments to customer feedback, <strong>QR codes for small business</strong> offer affordable, versatile solutions that previously required expensive custom apps.</p> - - <div class="my-8"> - <img src="/blog/small-business-body.png" alt="Customer scanning QR code at retail checkout" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>Top 10 QR Code Use Cases for Small Business</h2> - - <h3>1. Digital Menus & Product Catalogs</h3> - <p>Restaurants, cafĂ©s, and retail stores use QR codes to display menus and catalogs. Customers scan to view products, reducing print costs and enabling instant updates.</p> - <p>👉 <a href="/blog/qr-code-restaurant-menu">See our restaurant menu QR guide</a></p> - - <h3>2. Contactless Payments</h3> - <p>Link QR codes to payment platforms like PayPal, Venmo, or Square. Customers scan and pay without cash or card contact.</p> - - <h3>3. Google Reviews & Feedback</h3> - <p>Create QR codes linking directly to your Google Business review page. Place them on receipts, tables, or follow-up emails to boost review volume.</p> - - <h3>4. Business Cards & Networking</h3> - <p>Replace or enhance traditional business cards with <a href="/blog/vcard-qr-code-generator">vCard QR codes</a> that save contact info directly to phones.</p> - - <h3>5. Social Media Follows</h3> - <p>QR codes linking to Instagram, Facebook, or TikTok profiles help convert in-store visitors to online followers.</p> - - <h3>6. Appointment Booking</h3> - <p>Link to Calendly, Square Appointments, or your booking system. Perfect for salons, consultants, and service businesses.</p> - - <h3>7. Wi-Fi Access</h3> - <p>Create Wi-Fi QR codes for your business—customers scan to connect without asking for passwords.</p> - - <h3>8. Loyalty Programs</h3> - <p>QR codes can register loyalty program sign-ups or redeem points, enhancing customer retention.</p> - - <h3>9. Product Information</h3> - <p>Retail and e-commerce businesses add QR codes to packaging linking to tutorials, specifications, or warranty registration.</p> - - <h3>10. Event Tickets & Check-in</h3> - <p>Event businesses use QR codes as digital tickets for easy validation at entry points.</p> - - <h2>What to Look for in a Small Business QR Solution</h2> - <table class="w-full border-collapse my-6"> - <thead> - <tr class="bg-gray-100"> - <th class="border p-3 text-left">Feature</th> - <th class="border p-3 text-left">Why It Matters</th> - </tr> - </thead> - <tbody> - <tr><td class="border p-3">Dynamic QR Codes</td><td class="border p-3">Update URLs without reprinting</td></tr> - <tr><td class="border p-3">Scan Analytics</td><td class="border p-3">Measure campaign performance</td></tr> - <tr><td class="border p-3">Custom Branding</td><td class="border p-3">Match your brand identity</td></tr> - <tr><td class="border p-3">Bulk Creation</td><td class="border p-3">Create many codes from spreadsheets</td></tr> - <tr><td class="border p-3">Affordable Pricing</td><td class="border p-3">Budget-friendly for SMBs</td></tr> - <tr><td class="border p-3">No Expiration</td><td class="border p-3">Codes work forever (with active plan)</td></tr> - </tbody> - </table> - - <h2>QR Master for Small Business</h2> - <p>QR Master is designed with small businesses in mind:</p> - <ul> - <li><strong>Free Forever:</strong> Unlimited static QR codes, 3 free dynamic codes</li> - <li><strong>Pro Plan (€9/mo):</strong> 50 dynamic codes, full analytics, custom branding</li> - <li><strong>Business Plan (€29/mo):</strong> 500 codes, bulk creation, priority support</li> - </ul> - - <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> - <h3 class="text-xl font-semibold mb-2">Free Trial Available</h3> - <p>Start with our free plan—no credit card required. Upgrade when you need more dynamic codes or advanced features.</p> - </div> - - <h2>Getting Started: Quick Setup Guide</h2> - <ol> - <li><strong>Identify Your Goal:</strong> What do you want customers to do after scanning?</li> - <li><strong>Choose Code Type:</strong> Static for permanent content, dynamic for flexibility</li> - <li><strong>Create Your QR Code:</strong> Use <a href="/create">our generator</a> to design and customize</li> - <li><strong>Print at Right Size:</strong> Follow our <a href="/blog/qr-code-print-size-guide">print size guide</a></li> - <li><strong>Track Performance:</strong> Monitor scans in your <a href="/analytics">analytics dashboard</a></li> - </ol> - - <h2>Common Mistakes Small Businesses Make</h2> - <ul> - <li>❌ Using low-quality or blurry printed codes</li> - <li>❌ Linking to non-mobile-friendly pages</li> - <li>❌ Not testing codes before mass printing</li> - <li>❌ Choosing static codes when URLs might change</li> - <li>❌ Missing call-to-action near the QR code</li> - </ul> - - <h2>Conclusion</h2> - <p>QR codes offer small businesses powerful, affordable tools to enhance customer experiences and streamline operations. By choosing the right generator with dynamic capabilities and analytics, you can maximize your ROI and stay competitive in 2025.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Start Your QR Code Strategy Today</h3> - <p class="text-lg text-gray-700 mb-6">Join thousands of small businesses using QR Master for marketing, payments, and customer engagement.</p> - <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Get Started Free →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/blog/qr-code-restaurant-menu">Restaurant Menu QR Guide</a></li> - <li><a href="/blog/vcard-qr-code-generator">vCard Business Card Generator</a></li> - <li><a href="/blog/qr-code-analytics">QR Code Analytics Guide</a></li> - <li><a href="/pricing">View Pricing Plans</a></li> - </ul> - </div>`, - }, - - 'qr-code-print-size-guide': { - slug: 'qr-code-print-size-guide', - title: 'QR Code Print Size Guide: Minimum Sizes for Every Use Case', - excerpt: 'Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.', - date: 'January 5, 2026', - datePublished: '2026-01-05T12:00:00Z', - dateModified: '2026-01-05T12:00:00Z', - readTime: '8 Min', - category: 'Printing', - image: '/blog/qr-print-sizes.png', - imageAlt: 'Various print materials showing different QR code sizes', - author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.net/about', - answer: 'The minimum QR code size depends on scanning distance. For close scanning (business cards), minimum is 0.8" x 0.8" (2cm). For 6-foot distance (posters), minimum is 6" x 6" (15cm). Rule of thumb: QR size = scanning distance Ă· 10.', - content: `<div class="blog-content"> - <h2>Why QR Code Size Matters</h2> - <p>A QR code that's too small won't scan reliably, frustrating customers and wasting your printing investment. Understanding the relationship between <strong>QR code print size</strong>, scanning distance, and data density is essential for successful QR campaigns.</p> - - <h2>The Scanning Distance Formula</h2> - <p>The golden rule for QR code sizing:</p> - <div class="bg-gray-100 p-6 rounded-lg my-6 text-center"> - <p class="text-2xl font-bold text-gray-900">QR Code Width = Scanning Distance Ă· 10</p> - <p class="text-gray-600 mt-2">Example: 3 feet scanning distance = 3.6 inch QR code</p> - </div> - - <div class="my-8"> - <img src="/blog/qr-sizes-body.png" alt="Various QR code print sizes comparison" class="rounded-lg shadow-lg w-full" /> - </div> - - <h2>QR Code Sizes by Application</h2> - <table class="w-full border-collapse my-6"> - <thead> - <tr class="bg-gray-100"> - <th class="border p-3 text-left">Application</th> - <th class="border p-3 text-left">Scanning Distance</th> - <th class="border p-3 text-left">Minimum Size</th> - <th class="border p-3 text-left">Recommended</th> - </tr> - </thead> - <tbody> - <tr><td class="border p-3">Business Card</td><td class="border p-3">4-8 inches</td><td class="border p-3">0.8" (2cm)</td><td class="border p-3">1" (2.5cm)</td></tr> - <tr><td class="border p-3">Product Label</td><td class="border p-3">6-12 inches</td><td class="border p-3">0.6" (1.5cm)</td><td class="border p-3">1" (2.5cm)</td></tr> - <tr><td class="border p-3">Flyer/Brochure</td><td class="border p-3">1-2 feet</td><td class="border p-3">1.2" (3cm)</td><td class="border p-3">1.5" (4cm)</td></tr> - <tr><td class="border p-3">Table Tent</td><td class="border p-3">1-3 feet</td><td class="border p-3">2" (5cm)</td><td class="border p-3">2.5" (6cm)</td></tr> - <tr><td class="border p-3">Poster (indoor)</td><td class="border p-3">3-6 feet</td><td class="border p-3">4" (10cm)</td><td class="border p-3">6" (15cm)</td></tr> - <tr><td class="border p-3">Banner (outdoor)</td><td class="border p-3">6-15 feet</td><td class="border p-3">8" (20cm)</td><td class="border p-3">12" (30cm)</td></tr> - <tr><td class="border p-3">Billboard</td><td class="border p-3">15+ feet</td><td class="border p-3">18" (45cm)</td><td class="border p-3">24" (60cm)</td></tr> - </tbody> - </table> - - <h2>Factors Affecting Scanability</h2> - - <h3>1. Data Density</h3> - <p>More data = more modules = harder to scan at small sizes. Dynamic QR codes contain short redirect URLs, making them easier to scan at smaller sizes than static codes with long URLs.</p> - - <h3>2. Error Correction Level</h3> - <p>QR codes have four error correction levels:</p> - <ul> - <li><strong>L (7%):</strong> Smallest codes, least damage tolerance</li> - <li><strong>M (15%):</strong> Standard, good balance</li> - <li><strong>Q (25%):</strong> Higher tolerance, larger codes</li> - <li><strong>H (30%):</strong> Maximum tolerance, largest codes (needed for logos)</li> - </ul> - - <h3>3. Print Quality</h3> - <p>Low DPI printing blurs the code's modules. Recommended resolutions:</p> - <ul> - <li><strong>Minimum:</strong> 150 DPI</li> - <li><strong>Recommended:</strong> 300 DPI</li> - <li><strong>Best (small codes):</strong> 600 DPI</li> - </ul> - - <h3>4. Contrast</h3> - <p>Maintain minimum 3:1 contrast ratio between foreground and background. Avoid:</p> - <ul> - <li>Light gray on white</li> - <li>Similar color tones</li> - <li>Glossy surfaces with glare</li> - </ul> - - <h2>Quiet Zone Requirements</h2> - <p>The "quiet zone" is the blank margin around your QR code. Standard requirement:</p> - <div class="bg-gray-100 p-6 rounded-lg my-6 text-center"> - <p class="text-xl font-bold text-gray-900">Quiet Zone = 4 × Module Size</p> - <p class="text-gray-600 mt-2">Always leave white space around your QR code</p> - </div> - - <h2>File Formats for Printing</h2> - <table class="w-full border-collapse my-6"> - <thead> - <tr class="bg-gray-100"> - <th class="border p-3 text-left">Format</th> - <th class="border p-3 text-left">Best For</th> - <th class="border p-3 text-left">Scalability</th> - </tr> - </thead> - <tbody> - <tr><td class="border p-3">SVG</td><td class="border p-3">All print applications</td><td class="border p-3">∞ (vector)</td></tr> - <tr><td class="border p-3">PDF</td><td class="border p-3">Professional printing</td><td class="border p-3">∞ (vector)</td></tr> - <tr><td class="border p-3">PNG (300 DPI)</td><td class="border p-3">Digital and standard print</td><td class="border p-3">Limited</td></tr> - <tr><td class="border p-3">EPS</td><td class="border p-3">Professional design software</td><td class="border p-3">∞ (vector)</td></tr> - </tbody> - </table> - - <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> - <h3 class="text-xl font-semibold mb-2">Pro Tip: Always Use SVG</h3> - <p>Download your QR codes as SVG for infinite scalability. Scale up for billboards or down for business cards without losing quality.</p> - </div> - - <h2>Testing Before Printing</h2> - <p>Always test your QR codes before bulk printing:</p> - <ol> - <li>Print a test sample at actual size</li> - <li>Scan with multiple devices (iOS, Android)</li> - <li>Test from the intended scanning distance</li> - <li>Check under actual lighting conditions</li> - <li>Verify the destination URL works correctly</li> - </ol> - - <h2>Conclusion</h2> - <p>Proper QR code sizing ensures reliable scanning and protects your printing investment. Remember the distance Ă· 10 formula, always leave adequate quiet zones, and use vector formats for scalability. When in doubt, go slightly larger—a readable code is always better than a sleek but unscannable one.</p> - - <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> - <h3 class="text-2xl font-bold text-gray-900 mb-4">Create Print-Ready QR Codes</h3> - <p class="text-lg text-gray-700 mb-6">Download high-resolution SVG and PNG files ready for any print application.</p> - <a href="/create" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create QR Code →</a> - </div> - - <h2>Related Resources</h2> - <ul> - <li><a href="/blog/qr-code-restaurant-menu">Restaurant Menu QR Guide</a></li> - <li><a href="/blog/bulk-qr-codes-excel">Bulk QR Code Generation</a></li> - <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a></li> - </ul> - </div>`, - }, -}; - -function truncateAtWord(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - const truncated = text.slice(0, maxLength); - const lastSpace = truncated.lastIndexOf(' '); - return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; -} - -export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { - const post = blogPosts[params.slug]; - - if (!post) { - return { - title: 'Post Not Found', - }; - } - - const title = truncateAtWord(`${post.title} - QR Analytics Tips`, 60); - const description = truncateAtWord(post.excerpt, 160); - - return { - title, - description, - alternates: { - canonical: `https://www.qrmaster.net/blog/${params.slug}`, - languages: { - 'x-default': `https://www.qrmaster.net/blog/${params.slug}`, - en: `https://www.qrmaster.net/blog/${params.slug}`, - }, - }, - openGraph: { - title, - description, - url: `https://www.qrmaster.net/blog/${params.slug}`, - type: 'article', - publishedTime: post.datePublished, - modifiedTime: post.dateModified, - authors: [post.author], - images: [ - { - url: post.image, - width: 1200, - height: 630, - alt: post.imageAlt, - }, - ], - }, - twitter: { - title, - description, - card: 'summary_large_image', - images: [post.image], - }, - }; -} - -export default function BlogPostPage({ params }: { params: { slug: string } }) { - const post = blogPosts[params.slug]; - - if (!post) { - notFound(); - } - - const breadcrumbItems: BreadcrumbItem[] = [ - { name: 'Home', url: '/' }, - { name: 'Blog', url: '/blog' }, - { name: post.title, url: `/blog/${post.slug}` }, - ]; - - const schemas: any[] = [ - blogPostingSchema({ - title: post.title, - description: post.excerpt, - slug: post.slug, - author: post.author, - authorUrl: post.authorUrl, - datePublished: post.datePublished, - dateModified: post.dateModified, - image: post.image, - }), - breadcrumbSchema(breadcrumbItems), - ]; - - if (post.howTo) { - schemas.push(howToSchema(post.howTo)); - } - - return ( - <> - <SeoJsonLd data={schemas} /> - <div className="py-20 bg-gradient-to-b from-gray-50 to-white"> - <div className="container mx-auto px-4"> - <div className="max-w-4xl mx-auto"> - <Breadcrumbs items={breadcrumbItems} /> - - <article className="bg-white rounded-2xl shadow-sm p-8 md:p-12"> - <header className="mb-10"> - <div className="flex items-center flex-wrap gap-3 mb-6"> - <Badge variant="info">{post.category}</Badge> - <span className="text-gray-500 flex items-center"> - <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> - </svg> - {post.readTime} read - </span> - <span className="text-gray-500">By {post.author}</span> - <span className="text-gray-500">{post.date}</span> - </div> - - <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> - {post.title} - </h1> - - {post.answer && ( - <div className="bg-blue-50 border-l-4 border-blue-500 p-6 mb-8 rounded-r-lg"> - <h2 className="text-xl font-semibold mb-2 text-gray-900">Quick Answer</h2> - <p className="text-lg text-gray-800 leading-relaxed">{post.answer}</p> - </div> - )} - - <div className="relative w-full h-96 rounded-2xl overflow-hidden shadow-lg mb-8"> - <Image - src={post.image} - alt={post.imageAlt} - fill - className="object-cover" - priority - /> - </div> - </header> - - <div - className="prose prose-lg max-w-none - prose-headings:font-bold prose-headings:text-gray-900 - prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 - prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4 - prose-p:text-gray-700 prose-p:leading-relaxed prose-p:mb-6 prose-p:text-lg - prose-ul:my-6 prose-ul:space-y-2 - prose-li:text-gray-700 prose-li:leading-relaxed - prose-strong:text-gray-900 prose-strong:font-semibold" - dangerouslySetInnerHTML={{ __html: post.content }} - /> - - {post.howTo && ( - <div className="mt-12 bg-gradient-to-br from-blue-50 to-indigo-50 p-8 rounded-2xl border border-blue-200"> - <h2 className="text-3xl font-bold text-gray-900 mb-6">{post.howTo.name}</h2> - <p className="text-lg text-gray-700 mb-6 leading-relaxed">{post.howTo.description}</p> - <ol className="space-y-6"> - {post.howTo.steps.map((step: any, index: number) => ( - <li key={index} className="flex items-start"> - <span className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold text-lg mr-4"> - {index + 1} - </span> - <div className="flex-1"> - <h3 className="font-semibold text-xl mb-2 text-gray-900">{step.name}</h3> - <p className="text-gray-700 leading-relaxed">{step.text}</p> - </div> - </li> - ))} - </ol> - </div> - )} - - <div className="mt-16 p-10 bg-gradient-to-br from-primary-50 to-primary-100 rounded-2xl text-center border border-primary-200"> - <h2 className="text-3xl font-bold text-gray-900 mb-4"> - Ready to Track Your QR Campaigns? - </h2> - <p className="text-lg text-gray-700 mb-8 max-w-2xl mx-auto leading-relaxed"> - Start creating professional dynamic QR codes with advanced scan analytics, campaign tracking, and real-time insights. - </p> - <Link href="/signup"> - <Button size="lg">Create QR Code Free</Button> - </Link> - </div> - - {/* Related Articles Section */} - <div className="mt-16"> - <h2 className="text-2xl font-bold text-gray-900 mb-8">Related Articles</h2> - <div className="overflow-x-auto pb-4 -mx-4 px-4"> - <div className="flex gap-6" style={{ minWidth: 'max-content' }}> - {Object.values(blogPosts) - .filter((p) => p.slug !== post.slug) - .map((relatedPost) => ( - <Link - key={relatedPost.slug} - href={`/blog/${relatedPost.slug}`} - className="group block bg-gray-50 rounded-xl p-6 hover:bg-gray-100 transition-colors flex-shrink-0" - style={{ width: '320px' }} - > - <Badge variant="default" className="mb-3">{relatedPost.category}</Badge> - <h3 className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2"> - {relatedPost.title} - </h3> - <p className="text-sm text-gray-600 line-clamp-2">{relatedPost.excerpt}</p> - <span className="text-sm text-primary-600 mt-3 inline-block">Read more →</span> - </Link> - ))} - </div> - </div> - </div> - </article> - </div> - </div> - </div> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; +import { notFound } from 'next/navigation'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; +import { blogPostingSchema, breadcrumbSchema, howToSchema } from '@/lib/schema'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; + +interface BlogPostData { + slug: string; + title: string; + excerpt: string; + date: string; + datePublished: string; + dateModified: string; + readTime: string; + category: string; + image: string; + imageAlt: string; + author: string; + authorUrl: string; + answer?: string; + howTo?: any; + content: string; +} + +const blogPosts: Record<string, BlogPostData> = { + 'qr-code-analytics': { + slug: 'qr-code-analytics', + title: 'QR Code Analytics: Track, Measure & Optimize', + excerpt: 'Master scan analytics, campaign tracking & dashboard insights to maximize QR ROI with dynamic codes.', + date: 'October 16, 2025', + datePublished: '2025-10-16T09:00:00Z', + dateModified: '2025-10-16T09:00:00Z', + readTime: '15 Min', + category: 'Analytics', + image: '/blog/4-hero.png', + imageAlt: 'Smartphone displaying QR code scan with modern tech aesthetic', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'QR code analytics empowers marketers to track scan rates, user behavior, and campaign ROI through real-time dashboards, enabling data-driven optimization of dynamic QR codes and branded marketing campaigns.', + howTo: { + name: 'How to Track QR Code Scans', + description: 'Step-by-step guide to setting up and monitoring QR code analytics', + totalTime: 'PT10M', + steps: [ + { + name: 'Create a Dynamic QR Code', + text: 'Log into your QR Master dashboard and select "Create Dynamic QR Code". Enter your destination URL and customize design options.', + url: 'https://www.qrmaster.net/create', + }, + { + name: 'Enable UTM Tracking', + text: 'Add UTM parameters (source, medium, campaign) to track the QR code in Google Analytics and marketing platforms.', + }, + { + name: 'Access Analytics Dashboard', + text: 'Navigate to Dashboard → Analytics to view real-time scan data, geographic distribution, and device breakdowns.', + url: 'https://www.qrmaster.net/analytics', + }, + ], + }, + content: `<div class="blog-content"> + <h2>What Are Scan Analytics?</h2> + <p>Scan analytics provide comprehensive insights into how users interact with your QR codes. Our advanced dashboard tracks scan analytics including geographic location, device types, scan timestamps, and user engagement patterns. For marketers running dynamic QR code campaigns, these insights are essential for understanding campaign tracking performance and optimizing conversion rates.</p> + <p>With branded QR codes deployed across print materials, event tickets, and business cards, scan analytics reveal which channels drive the highest engagement. Security features ensure all data collection is GDPR-compliant, protecting user privacy while delivering actionable campaign tracking insights.</p> + + <h2>How to Set Up QR Code Analytics</h2> + <h3>Step 1: Create a Dynamic QR Code</h3> + <p>Start by generating a dynamic QR code in your QR Master dashboard. Unlike static codes, dynamic QR codes allow you to update destination URLs and track every scan through our analytics platform.</p> + + <h3>Step 2: Enable Campaign Tracking</h3> + <p>Configure UTM parameters for your QR codes to integrate with Google Analytics and marketing automation platforms. UTM tracking allows you to attribute conversions, measure ROI, and segment campaign performance by source, medium, and campaign name.</p> + + <h3>Step 3: Access Your Analytics Dashboard</h3> + <p>Navigate to the scan analytics dashboard to view real-time reports. Monitor scan rates, geographic distribution, device breakdowns, and time-series data. Set up automated reports to track campaign tracking metrics over time.</p> + + <h3>Step 4: Optimize Based on Insights</h3> + <p>Use scan analytics to identify high-performing campaigns and optimize underperforming ones. A/B test different branded QR designs, placement strategies, and call-to-action messaging to maximize engagement and conversion rates.</p> + + <h2>Key Metrics in QR Code Analytics</h2> + <h3>Scan Rates and Volume</h3> + <p>Track total scans, unique scans, and scan velocity. Scan rates reveal campaign momentum and help identify viral growth patterns. Compare scan volumes across different branded QR variations to determine which designs perform best.</p> + + <h3>Geographic Distribution</h3> + <p>Understand where your audience is scanning from. Geographic analytics support localized marketing strategies and event tracking for conferences, trade shows, and retail activations.</p> + + <h3>Device and Browser Analytics</h3> + <p>Know whether users scan from iOS or Android devices, which browsers they use, and screen resolutions. This data informs mobile optimization strategies and ensures your landing pages deliver seamless experiences across all devices.</p> + + <h3>Time-Based Patterns</h3> + <p>Identify peak scanning hours, days of the week, and seasonal trends. Time-based analytics optimize campaign timing for email blasts, social media posts, and print QR deployments.</p> + + <h3>Conversion Tracking</h3> + <p>Measure downstream actions after the scan—form submissions, purchases, app downloads, or content engagement. Integrate with your CRM and marketing stack to attribute revenue to specific QR campaigns.</p> + + <div class="my-8"> + <img src="/blog/4-body.png" alt="Team meeting analyzing QR code data in office" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>Advanced Campaign Tracking Strategies</h2> + <h3>UTM Tracking Integration</h3> + <p>Append UTM parameters to your dynamic QR URLs for granular campaign attribution. Use consistent naming conventions across campaigns to compare performance in Google Analytics. UTM tracking bridges offline and online marketing, providing a unified view of customer journeys.</p> + + <h3>Multi-Channel Attribution</h3> + <p>Deploy branded QR codes across print ads, packaging, event tickets, business cards, and signage. Use unique QR codes for each channel to measure which touchpoints drive the highest ROI. Multi-channel attribution reveals the true value of integrated marketing campaigns.</p> + + <h3>A/B Testing QR Designs</h3> + <p>Test different branded QR styles—color schemes, logo placements, and call-to-action text—to optimize scan rates. Our analytics dashboard makes it easy to compare performance and roll out winning variations at scale.</p> + + <h3>Retargeting and Remarketing</h3> + <p>Leverage scan analytics to build retargeting audiences. Users who scan but don't convert can be re-engaged with display ads, email campaigns, and social media retargeting, boosting overall campaign ROI.</p> + + <h2>Security and Compliance in QR Analytics</h2> + <p>All QR Master scan analytics are GDPR-compliant, ensuring user data is collected, stored, and processed securely. We employ enterprise-grade security protocols to protect sensitive campaign data, making our platform ideal for bulk QR generation workflows in regulated industries.</p> + <p>Secure QR codes prevent unauthorized access and malicious redirects. Our platform includes link validation, SSL encryption, and fraud detection to maintain trust and protect your brand reputation.</p> + + <h2>Use Cases for QR Code Analytics</h2> + <h3>Event Tracking</h3> + <p>Deploy QR codes on event tickets, badges, and signage to track attendee engagement. Scan analytics reveal which sessions attract the most interest, optimize check-in flows, and measure event ROI.</p> + + <h3>Print Marketing Campaigns</h3> + <p>Use QR codes in magazine ads, direct mail, and packaging to bridge offline and online channels. Campaign tracking quantifies print campaign performance and justifies marketing spend.</p> + + <h3>Business Card Analytics</h3> + <p>Add dynamic QR codes to business cards to track networking effectiveness. Scan analytics show how many contacts engage, when they scan, and which follow-up actions they take.</p> + + <h3>Bulk QR Generation for Retail</h3> + <p>Generate thousands of product QR codes with our bulk QR tool. Track scan analytics at the SKU level to understand customer interest, optimize inventory, and personalize marketing.</p> + + <h3>API-Driven Automation</h3> + <p>Integrate QR code generation and analytics into your marketing automation platform via our API. Automate bulk QR creation, dynamic URL updates, and reporting workflows for enterprise-scale campaigns.</p> + + <h2>Maximizing ROI with Scan Analytics</h2> + <p>To maximize QR code ROI, continuously monitor scan analytics and iterate on campaign strategies. Test different branded QR designs, optimize UTM parameters, and leverage multi-channel attribution to understand the full customer journey.</p> + <p>Combine scan analytics with customer data platforms (CDPs) and CRMs to personalize follow-up communications. Segment audiences based on scan behavior and deliver targeted offers that drive conversions.</p> + <p>For bulk QR campaigns, use our analytics dashboard to identify trends across thousands of codes. Aggregate data reveals macro patterns while code-level metrics enable micro-optimizations.</p> + + <h2>Conclusion</h2> + <p>QR code analytics transforms QR codes from simple links into powerful marketing instruments. By tracking scan rates, user behavior, and campaign performance through advanced dashboards, marketers gain the insights needed to optimize dynamic QR campaigns, enhance branded experiences, and achieve measurable ROI.</p> + <p>Whether you're deploying QR codes for event tracking, print marketing, bulk generation, or API-driven automation, scan analytics provides the data foundation for smarter, more effective campaigns. Start leveraging QR analytics today to unlock the full potential of your QR marketing strategy.</p> + </div>`, + }, + 'qr-code-tracking-guide-2025': { + slug: 'qr-code-tracking-guide-2025', + title: 'QR Code Tracking: Complete Guide 2025 (Free Tools & Best Practices)', + excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools, setup Google Analytics, and measure ROI. Complete guide 2025.', + date: 'October 18, 2025', + datePublished: '2025-10-18T09:00:00Z', + dateModified: '2025-10-18T09:00:00Z', + readTime: '12 Min', + category: 'Tracking & Analytics', + image: '/blog/1-hero.png', + imageAlt: 'QR code tracking and analytics visualization', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'QR code tracking allows you to monitor scan metrics including location, device type, time, and user behavior using dynamic QR codes. Only dynamic QR codes can be tracked—static codes cannot provide analytics. Use tools like QR Master, Google Analytics with UTM parameters, or URL shorteners to track scans and measure campaign ROI effectively.', + howTo: { + name: 'How to Set Up QR Code Tracking', + description: 'Complete step-by-step guide to tracking QR code scans with analytics', + totalTime: 'PT15M', + steps: [ + { + name: 'Create a Dynamic QR Code', + text: 'Sign up for QR Master and create a dynamic QR code. Enter your destination URL and customize the design with your brand colors and logo.', + url: 'https://www.qrmaster.net/signup', + }, + { + name: 'Add UTM Parameters', + text: 'Configure UTM tracking parameters: utm_source=qr, utm_medium=print, utm_campaign=your-campaign-name. This enables tracking in Google Analytics.', + }, + { + name: 'Deploy Your QR Code', + text: 'Download the QR code and place it on your marketing materials: print ads, product packaging, business cards, or event posters.', + }, + { + name: 'Monitor Analytics Dashboard', + text: 'Access your QR Master dashboard to view real-time scan data: total scans, unique users, geographic location, device types, and scan timestamps.', + url: 'https://www.qrmaster.net/analytics', + }, + { + name: 'Optimize Based on Data', + text: 'Analyze scan patterns to optimize your campaigns. Test different placements, designs, and calls-to-action to improve scan rates and conversion.', + }, + ], + }, + content: `<div class="blog-content"> + <p>QR code tracking is essential for measuring the success of your marketing campaigns. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented in 1994 by Masahiro Hara at Denso Wave, and have evolved from automotive tracking to powerful marketing tools with advanced analytics capabilities. In this comprehensive guide, you'll learn everything about tracking QR code scans, from basic setup to advanced campaign optimization.</p> + + <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> + <h3 class="text-xl font-semibold mb-2 text-gray-900">Quick Takeaway</h3> + <p class="text-gray-800">Only <strong>dynamic QR codes</strong> can be tracked. Static QR codes encode data directly and provide no analytics. To track scans, you must use a dynamic QR code that redirects through a server that logs scan data. QR Master offers unlimited tracking with detailed analytics on every scan.</p> + </div> + + <h2>What is QR Code Tracking?</h2> + <p>QR code tracking is the process of monitoring and analyzing scan data from QR codes to measure campaign performance and user behavior. When someone scans a trackable QR code, the system captures valuable data including:</p> + + <ul> + <li><strong>Scan count:</strong> Total scans and unique scans</li> + <li><strong>Location data:</strong> Country, city, and region of the scanner</li> + <li><strong>Device information:</strong> iOS vs Android, device model, operating system version</li> + <li><strong>Time and date:</strong> When scans occur (hour, day, week, month)</li> + <li><strong>Referrer source:</strong> Where the scan originated (if tracked)</li> + <li><strong>User behavior:</strong> Actions taken after scanning (page views, conversions, purchases)</li> + </ul> + + <h3>Static vs Dynamic QR Codes: Why Tracking Matters</h3> + <p>Understanding the difference between static and dynamic QR codes is crucial for tracking:</p> + + <p><strong>Static QR Codes:</strong> These encode the destination URL directly into the QR code pattern. Once generated, the content cannot be changed, and no tracking is possible. The QR code reader goes directly to the encoded destination without any intermediate server.</p> + + <p><strong>Dynamic QR Codes:</strong> These contain a short redirect URL (like qrmaster.net/abc123) that points to a server. The server logs the scan data and then redirects to your actual destination URL. This enables tracking AND allows you to change the destination URL anytime—even after printing thousands of codes.</p> + + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Feature</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Static QR</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Dynamic QR</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Track Scans</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Edit After Printing</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Analytics Dashboard</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Cost</td> + <td class="border border-gray-300 px-6 py-4">Free</td> + <td class="border border-gray-300 px-6 py-4">Free - $29/month</td> + </tr> + </tbody> + </table> + </div> + + <h2>Why Track QR Codes? Key Benefits</h2> + + <h3>1. Measure Marketing ROI</h3> + <p>QR code tracking provides concrete data on campaign performance. Instead of guessing how many people engaged with your print ad, you get exact numbers. Calculate cost per scan: if your billboard costs $5,000/month and generates 10,000 scans, that's $0.50 per engagement—compare that to your digital ad costs.</p> + + <p><strong>Real-world example:</strong> A retail brand placed QR codes on product packaging linking to warranty registration. By tracking scans, they discovered only 12% of customers registered warranties. They A/B tested different incentives (15% off next purchase vs extended warranty) and increased registration to 34%—all measured through QR code tracking.</p> + + <h3>2. Understand Your Audience</h3> + <p>QR code analytics reveal WHO is scanning your codes:</p> + <ul> + <li><strong>Geographic insights:</strong> Are most scans from your local area or nationwide? This helps optimize ad placement.</li> + <li><strong>Device data:</strong> 70% iOS users might indicate an affluent audience; optimize your landing page accordingly.</li> + <li><strong>Time patterns:</strong> Peak scanning at 7-9 PM? Schedule your social media posts and email campaigns for those hours.</li> + </ul> + + <h3>3. Optimize Product Engagement</h3> + <p>For e-commerce and retail, QR codes on packaging track which products generate the most engagement. If Product A gets 5x more scans than Product B, you know customers are more interested in learning about Product A—perhaps it needs clearer instructions, or customers want to see user reviews.</p> + + <h3>4. Event Management & Attendance Tracking</h3> + <p>Event tickets with unique QR codes enable real-time attendance tracking. Scan QR codes at check-in to see who arrived, prevent duplicate entries, and measure session attendance. Post-event, analyze which sessions were most popular and optimize future events.</p> + + <h2>How to Track QR Code Scans: 4 Methods</h2> + + <h3>Method 1: Using Dynamic QR Code Generators (Recommended)</h3> + <p>The easiest method is using a dedicated QR code platform like QR Master. These services provide built-in tracking without any technical setup.</p> + + <h4>Step-by-Step with QR Master:</h4> + <ol> + <li><strong>Sign up for free:</strong> Create your QR Master account at <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li> + <li><strong>Create dynamic QR code:</strong> Click "Create QR Code" and select "Dynamic QR"</li> + <li><strong>Enter destination URL:</strong> Add the website, landing page, or content you want to link</li> + <li><strong>Customize design:</strong> Add your logo, brand colors, and custom frame</li> + <li><strong>Download and deploy:</strong> Get high-resolution PNG or SVG for print</li> + <li><strong>Access analytics:</strong> Go to Dashboard → Analytics to view real-time scan data</li> + </ol> + + <p><strong>Benefits:</strong> No coding required, instant setup, real-time dashboard, unlimited scans on paid plans, GDPR compliant.</p> + + <h3>Method 2: Google Analytics with UTM Parameters</h3> + <p>If you're already using Google Analytics, you can track QR codes using UTM parameters. This method works with both static and dynamic QR codes, but you won't get device-specific data—only what Google Analytics provides.</p> + + <h4>How to Set Up UTM Tracking:</h4> + <ol> + <li><strong>Build your UTM URL:</strong> Use Google's Campaign URL Builder</li> + <li><strong>Add parameters:</strong> + <ul> + <li>utm_source=qr</li> + <li>utm_medium=print (or offline, packaging, etc.)</li> + <li>utm_campaign=summer2025 (your campaign name)</li> + </ul> + </li> + <li><strong>Example:</strong> <code>https://yoursite.com?utm_source=qr&utm_medium=print&utm_campaign=summer2025</code></li> + <li><strong>Generate QR code:</strong> Create QR code from this UTM-tagged URL</li> + <li><strong>Track in Google Analytics:</strong> Go to Acquisition → Campaigns to view QR code traffic</li> + </ol> + + <p><strong>Limitations:</strong> No device-specific data, no real-time scan count, cannot edit URL after printing (unless you use dynamic QR codes WITH UTM parameters—best of both worlds).</p> + + <h3>Method 3: URL Shorteners with Analytics</h3> + <p>Services like Bitly, TinyURL, and Rebrandly offer URL shortening with basic analytics. Create a short link, generate a QR code from it, and track clicks in the shortener's dashboard.</p> + + <h4>Pros:</h4> + <ul> + <li>Free tier available (Bitly: 1,000 links free)</li> + <li>Simple setup, no coding</li> + <li>Basic analytics: click count, geographic data</li> + </ul> + + <h4>Cons:</h4> + <ul> + <li>Limited customization</li> + <li>Less detailed analytics than dedicated QR platforms</li> + <li>Cannot add logo or branding to QR code itself</li> + <li>Links might look generic (bit.ly/abc123) rather than branded</li> + </ul> + + <h3>Method 4: Self-Hosted Tracking (Advanced)</h3> + <p>For developers or enterprises with specific requirements, build your own QR redirect system. Use Node.js, PHP, Python, or serverless functions (AWS Lambda, Cloudflare Workers) to create a custom redirect server that logs scan data to your database.</p> + + <h4>Basic Implementation (Node.js Example):</h4> + <pre><code> +app.get('/qr/:id', async (req, res) => { + const qrId = req.params.id; + + // Log scan data + await logScan({ + qrId, + timestamp: new Date(), + ip: req.ip, + userAgent: req.get('user-agent'), + referrer: req.get('referer') + }); + + // Get destination URL from database + const destination = await getDestination(qrId); + + // Redirect to destination + res.redirect(302, destination); +}); + </code></pre> + + <p><strong>Privacy Note:</strong> Always hash IP addresses, respect Do Not Track headers, and comply with GDPR when collecting scan data.</p> + + <div class="my-8"> + <img src="/blog/1-boy.png" alt="Person scanning QR code with smartphone in office" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>QR Code Tracking Tools Comparison</h2> + + <h3>Free Tools:</h3> + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Tool</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Scans/Month</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Analytics</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Custom Domain</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Price</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Free</td> + <td class="border border-gray-300 px-6 py-4">Unlimited</td> + <td class="border border-gray-300 px-6 py-4">Full Dashboard</td> + <td class="border border-gray-300 px-6 py-4">No</td> + <td class="border border-gray-300 px-6 py-4">$0</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Google Analytics</td> + <td class="border border-gray-300 px-6 py-4">Unlimited</td> + <td class="border border-gray-300 px-6 py-4">Full (with GA4)</td> + <td class="border border-gray-300 px-6 py-4">Yes</td> + <td class="border border-gray-300 px-6 py-4">$0</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Bitly Free</td> + <td class="border border-gray-300 px-6 py-4">1,000</td> + <td class="border border-gray-300 px-6 py-4">Basic</td> + <td class="border border-gray-300 px-6 py-4">No</td> + <td class="border border-gray-300 px-6 py-4">$0</td> + </tr> + </tbody> + </table> + </div> + + <h3>Paid Tools:</h3> + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Tool</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Scans/Month</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Analytics</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Custom Domain</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Price</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Pro</td> + <td class="border border-gray-300 px-6 py-4">Unlimited</td> + <td class="border border-gray-300 px-6 py-4">Advanced</td> + <td class="border border-gray-300 px-6 py-4">Yes</td> + <td class="border border-gray-300 px-6 py-4">$9/mo</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Code Generator</td> + <td class="border border-gray-300 px-6 py-4">Unlimited</td> + <td class="border border-gray-300 px-6 py-4">Full</td> + <td class="border border-gray-300 px-6 py-4">Yes</td> + <td class="border border-gray-300 px-6 py-4">$12/mo</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Beaconstac</td> + <td class="border border-gray-300 px-6 py-4">Unlimited</td> + <td class="border border-gray-300 px-6 py-4">Advanced</td> + <td class="border border-gray-300 px-6 py-4">Yes</td> + <td class="border border-gray-300 px-6 py-4">$49/mo</td> + </tr> + </tbody> + </table> + </div> + + <p><strong>Our Recommendation:</strong></p> + <ul> + <li><strong>For beginners:</strong> Start with QR Master Free + Google Analytics</li> + <li><strong>For small businesses:</strong> QR Master Pro at $9/month (best value)</li> + <li><strong>For enterprises:</strong> Beaconstac or custom solution</li> + </ul> + + <h2>QR Code Tracking Best Practices</h2> + + <h3>1. Always Use Dynamic QR Codes for Campaigns</h3> + <p>Static QR codes cannot be tracked or edited. If you're printing 1,000+ codes or spending significant money on the campaign, always use dynamic QR codes. The small monthly cost ($9-29) is negligible compared to reprint costs if the URL changes.</p> + + <h3>2. Set Clear Goals Before Tracking</h3> + <p>Define what success looks like before launching your campaign:</p> + <ul> + <li>Target scan count: "We want 500+ scans in the first month"</li> + <li>Conversion goal: "30% of scanners should sign up for newsletter"</li> + <li>Geographic goal: "Focus on scans from NYC metro area"</li> + </ul> + + <h3>3. Use Consistent UTM Naming Conventions</h3> + <p>Standardize your UTM parameters across all QR codes:</p> + <ul> + <li>utm_source: Always "qr"</li> + <li>utm_medium: Specific placement ("billboard", "packaging", "flyer", "business-card")</li> + <li>utm_campaign: Campaign name ("summer2025", "product-launch", "event-ticket")</li> + </ul> + <p>Example: <code>utm_source=qr&utm_medium=billboard&utm_campaign=summer2025</code></p> + + <h3>4. Test Before Printing</h3> + <p>Before sending 10,000 codes to the printer:</p> + <ul> + <li>Scan QR code with multiple devices (iOS, Android)</li> + <li>Verify tracking is working in your dashboard</li> + <li>Check landing page loads fast on mobile (<3 seconds)</li> + <li>Test different lighting conditions and distances</li> + </ul> + + <h3>5. Monitor Regularly</h3> + <p>Don't just "set and forget" your QR codes:</p> + <ul> + <li><strong>Daily:</strong> Check for the first week to catch any issues early</li> + <li><strong>Weekly:</strong> Review scan trends during active campaigns</li> + <li><strong>Monthly:</strong> Analyze long-term patterns and create reports</li> + </ul> + + <h3>6. Privacy & GDPR Compliance</h3> + <p>Respect user privacy when collecting scan data:</p> + <ul> + <li><strong>Hash IP addresses:</strong> Don't store raw IPs; hash them for privacy</li> + <li><strong>Respect Do Not Track:</strong> Honor DNT browser headers</li> + <li><strong>Privacy policy:</strong> Mention QR tracking in your privacy policy</li> + <li><strong>GDPR compliance:</strong> Allow EU users to request data deletion</li> + <li><strong>Cookie consent:</strong> If your landing page uses cookies, show consent banner</li> + </ul> + + <h2>QR Code Tracking Use Cases</h2> + + <h3>1. Restaurant Menus</h3> + <p><strong>Scenario:</strong> Restaurant replaces physical menus with QR codes on tables.</p> + <p><strong>Tracking Benefits:</strong></p> + <ul> + <li>See which tables generate most scans (optimize table placement)</li> + <li>Track peak scanning times (staff accordingly)</li> + <li>A/B test different menu designs to increase order value</li> + <li>Measure return scan rate (customer loyalty indicator)</li> + </ul> + + <h3>2. Retail Product Packaging</h3> + <p><strong>Scenario:</strong> Product packaging includes QR code linking to warranty registration.</p> + <p><strong>Tracking Benefits:</strong></p> + <ul> + <li>Track which products have highest engagement</li> + <li>Measure warranty registration completion rate</li> + <li>Identify geographic markets with strong sales</li> + <li>A/B test incentives (discount codes vs extended warranty)</li> + </ul> + + <h3>3. Event Tickets</h3> + <p><strong>Scenario:</strong> Conference tickets feature unique QR codes for check-in.</p> + <p><strong>Tracking Benefits:</strong></p> + <ul> + <li>Real-time attendance tracking</li> + <li>Prevent duplicate check-ins (fraud prevention)</li> + <li>Track session attendance by placing QR at session doors</li> + <li>Post-event analysis: which sessions were most popular?</li> + </ul> + + <h3>4. Real Estate Signs</h3> + <p><strong>Scenario:</strong> For Sale signs include QR code to property details.</p> + <p><strong>Tracking Benefits:</strong></p> + <ul> + <li>Measure property interest (scan count = qualified leads)</li> + <li>Track which neighborhoods generate most interest</li> + <li>Capture leads automatically (link to contact form)</li> + <li>A/B test different signage designs</li> + </ul> + + <h3>5. Print Advertising</h3> + <p><strong>Scenario:</strong> Magazine ad includes QR code to special offer.</p> + <p><strong>Tracking Benefits:</strong></p> + <ul> + <li>Calculate cost per scan (ad cost Ă· scans)</li> + <li>Compare performance across different magazines</li> + <li>Track which ad creative generates most scans</li> + <li>Measure conversion rate from scan to purchase</li> + </ul> + + <h2>Advanced QR Code Tracking Strategies</h2> + + <h3>A/B Testing QR Code Designs</h3> + <p>Don't just guess which QR design performs best—test it. Create two versions:</p> + <ul> + <li><strong>Version A:</strong> Plain black and white QR</li> + <li><strong>Version B:</strong> Branded QR with logo and custom colors</li> + </ul> + <p>Deploy equal quantities of each and track which generates more scans. Many brands find that branded QR codes get 30-50% higher scan rates because they look more trustworthy.</p> + + <h3>Multi-Channel Attribution</h3> + <p>Use unique QR codes for each marketing channel to measure which drives the best results:</p> + <ul> + <li>Billboard: <code>qrmaster.net/billboard-nyc</code></li> + <li>Magazine ad: <code>qrmaster.net/magazine-vogue</code></li> + <li>Product packaging: <code>qrmaster.net/packaging-productA</code></li> + <li>Business card: <code>qrmaster.net/card-john</code></li> + </ul> + <p>Track scans separately to calculate ROI per channel.</p> + + <h3>Retargeting Scanners Who Don't Convert</h3> + <p>Add retargeting pixels (Facebook Pixel, Google Ads remarketing) to your QR code landing page. Users who scan but don't convert can be retargeted with ads:</p> + <ul> + <li>"Still interested? Get 15% off today"</li> + <li>"You left something in your cart..."</li> + <li>"Here's what you were looking for"</li> + </ul> + <p>This dramatically improves overall campaign ROI.</p> + + <h3>Geographic Targeting</h3> + <p>If QR tracking shows 70% of scans come from California, optimize your campaigns:</p> + <ul> + <li>Show California-specific content on landing page</li> + <li>Offer California-only promotions</li> + <li>Increase ad spend in California, decrease elsewhere</li> + <li>Open physical retail in high-scan regions</li> + </ul> + + <h2>Common QR Code Tracking Mistakes to Avoid</h2> + + <h3>Mistake 1: Using Static QR Codes for Campaigns</h3> + <p>Static QR codes cannot be tracked or edited. If you print 5,000 flyers with a static QR and the URL changes, you're stuck. Always use dynamic QR codes for any quantity over 100.</p> + + <h3>Mistake 2: Not Mobile-Optimizing Landing Pages</h3> + <p>100% of QR code scans come from mobile devices. If your landing page isn't mobile-friendly, you'll lose 50-70% of potential conversions. Test on real devices before launching.</p> + + <h3>Mistake 3: Ignoring Privacy Regulations</h3> + <p>Collecting scan data without proper consent can result in GDPR fines up to €20 million. Always:</p> + <ul> + <li>Include QR tracking in your privacy policy</li> + <li>Obtain consent for cookies on landing page</li> + <li>Allow users to opt-out of tracking</li> + <li>Hash or anonymize IP addresses</li> + </ul> + + <h3>Mistake 4: Setting Unrealistic Scan Goals</h3> + <p>Typical QR code scan rates:</p> + <ul> + <li><strong>Business cards:</strong> 5-15% scan rate</li> + <li><strong>Product packaging:</strong> 1-5% scan rate</li> + <li><strong>Restaurant tables:</strong> 30-70% scan rate (motivated users want menu)</li> + <li><strong>Print ads:</strong> 0.5-2% scan rate</li> + </ul> + <p>Set goals based on industry benchmarks, not wishful thinking.</p> + + <h3>Mistake 5: Not Testing Before Printing</h3> + <p>One small mistake—wrong URL, broken link, slow loading page—can ruin an entire campaign. Always test:</p> + <ul> + <li>Scan from iOS and Android</li> + <li>Verify destination URL is correct</li> + <li>Check mobile page load speed (<3 seconds)</li> + <li>Confirm tracking is working in dashboard</li> + </ul> + + <h2>QR Code Tracking Metrics to Monitor</h2> + + <h3>Primary Metrics:</h3> + <ul> + <li><strong>Total scans:</strong> Raw number of all scans</li> + <li><strong>Unique scans:</strong> Number of individual users (more important than total)</li> + <li><strong>Scan rate:</strong> Scans Ă· potential impressions</li> + <li><strong>Conversion rate:</strong> Conversions Ă· scans</li> + <li><strong>Cost per scan:</strong> Campaign cost Ă· total scans</li> + <li><strong>ROI:</strong> (Revenue - Cost) Ă· Cost × 100%</li> + </ul> + + <h3>Secondary Metrics:</h3> + <ul> + <li><strong>Geographic distribution:</strong> Where scanners are located</li> + <li><strong>Device breakdown:</strong> iOS vs Android percentage</li> + <li><strong>Time patterns:</strong> Peak scanning hours/days</li> + <li><strong>Referrer data:</strong> How users discovered the QR code</li> + <li><strong>Bounce rate:</strong> % who leave immediately after scanning</li> + <li><strong>Average session duration:</strong> Time spent on landing page</li> + </ul> + + <h2>Conclusion</h2> + <p>QR code tracking transforms simple codes into powerful marketing instruments. By implementing the strategies in this guide—using dynamic QR codes, setting up proper analytics, following best practices, and continuously optimizing—you can measure and improve campaign ROI dramatically.</p> + + <p>Remember:</p> + <ul> + <li>Always use <strong>dynamic QR codes</strong> for tracking (static cannot be tracked)</li> + <li>Start with <strong>free tools</strong> like QR Master Free + Google Analytics</li> + <li>Set <strong>clear goals</strong> before launching campaigns</li> + <li>Test <strong>thoroughly</strong> before printing large quantities</li> + <li>Monitor <strong>regularly</strong> and optimize based on data</li> + <li>Respect <strong>privacy regulations</strong> (GDPR, CCPA)</li> + </ul> + + <p>Whether you're tracking restaurant menus, product packaging, event tickets, or print advertising, QR code analytics provides the insights needed to justify marketing spend and improve performance. Start tracking your QR codes today and unlock data-driven marketing success.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Ready to Start Tracking?</h3> + <p class="text-lg text-gray-700 mb-6">Create your first trackable dynamic QR code in 60 seconds. Free plan includes unlimited scans with full analytics dashboard.</p> + <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create Free QR Code Now →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a> - Create trackable QR codes</li> + <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a> - Learn the difference</li> + <li><a href="/blog/bulk-qr-code-generator-excel">Bulk QR Code Generator</a> - Generate hundreds at once</li> + <li><a href="/pricing">Pricing Plans</a> - Compare free and paid options</li> + <li><a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">QR Code on Wikipedia</a> - Technical details and history</li> + </ul> + </div>`, + }, + 'dynamic-vs-static-qr-codes': { + slug: 'dynamic-vs-static-qr-codes', + title: 'Dynamic vs Static QR Codes: Which Should You Use? Complete Comparison 2025', + excerpt: 'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money. Expert guide 2025.', + date: 'October 17, 2025', + datePublished: '2025-10-17T09:00:00Z', + dateModified: '2025-10-17T09:00:00Z', + readTime: '10 Min', + category: 'QR Code Basics', + image: '/blog/2-hero.png', + imageAlt: 'Two QR codes side by side showing static and dynamic comparison', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'Static QR codes encode data directly and cannot be edited after creation, while dynamic QR codes contain a short redirect URL that can be updated anytime. Dynamic QR codes also provide tracking analytics, making them ideal for marketing campaigns. Static QR codes work forever without subscriptions, perfect for permanent content like contact cards or fixed URLs.', + content: `<div class="blog-content"> + <p>Choosing between static and dynamic QR codes is one of the most important decisions when implementing a QR code strategy. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented in 1994 by Masahiro Hara at Denso Wave for automotive part tracking. Today, QR codes have evolved into sophisticated marketing tools, with dynamic QR codes offering features unimaginable in their original static form.</p> + + <p>This comprehensive guide explains the critical differences between static and dynamic QR codes, helping you choose the right type for your specific needs. Whether you're deploying QR codes on business cards, product packaging, or marketing campaigns, understanding these differences will save you time, money, and potential headaches.</p> + + <h2>What is a Static QR Code?</h2> + + <p>A static QR code directly encodes your data into the QR code pattern itself. When you create a static QR code for a URL, that URL is permanently embedded in the black-and-white squares. The QR code reader decodes the pattern and accesses the content directly—no intermediate server, no redirect, no tracking.</p> + + <h3>How Static QR Codes Work</h3> + <p>Think of a static QR code like printing a phone number on a business card. The phone number is the final information—there's no lookup service or translation layer. When someone scans the QR code, their device reads the encoded data and immediately processes it (opens the URL, displays the text, opens a location in maps, etc.).</p> + + <p><strong>Example:</strong> If you create a static QR code for <code>https://www.yourwebsite.com/summer-sale-2025</code>, that exact URL is encoded into the QR code pattern. The QR code scanner extracts this URL and opens it directly.</p> + + <h3>Common Uses for Static QR Codes</h3> + <ul> + <li><strong>Contact cards (vCard):</strong> Share permanent contact information on business cards</li> + <li><strong>Location links:</strong> Direct links to Google Maps locations for offices or stores</li> + <li><strong>App store links:</strong> Fixed URLs that never change</li> + <li><strong>Bitcoin wallet addresses:</strong> Cryptocurrency payment addresses</li> + <li><strong>Fixed website URLs:</strong> Company homepage, about page, etc.</li> + <li><strong>Text messages or phone numbers:</strong> "Text HELP to 12345"</li> + </ul> + + <h3>Advantages of Static QR Codes</h3> + <div class="bg-green-50 border-l-4 border-green-500 p-6 my-6 rounded-r-lg"> + <ul> + <li><strong>✅ Works forever:</strong> No dependency on external servers or subscriptions. Once created, it functions permanently.</li> + <li><strong>✅ Faster scanning:</strong> No redirect delay—scanner goes directly to content (typically 100-300ms faster than dynamic).</li> + <li><strong>✅ Works offline:</strong> For content types like vCards or location data, no internet connection needed for initial scan.</li> + <li><strong>✅ Completely free:</strong> No ongoing costs or subscriptions required.</li> + <li><strong>✅ Privacy-friendly:</strong> No tracking, no data collection, no third-party involvement.</li> + <li><strong>✅ Simple:</strong> What you encode is what you get—no complexity.</li> + </ul> + </div> + + <h3>Disadvantages of Static QR Codes</h3> + <div class="bg-red-50 border-l-4 border-red-500 p-6 my-6 rounded-r-lg"> + <ul> + <li><strong>❌ Cannot edit after printing:</strong> If the URL changes or contains a typo, you must reprint all QR codes.</li> + <li><strong>❌ No analytics:</strong> Impossible to track scan count, location, device, or user behavior.</li> + <li><strong>❌ Long URLs create complex codes:</strong> Longer URLs = more data = denser, harder-to-scan QR codes.</li> + <li><strong>❌ No A/B testing:</strong> Cannot test different destinations without creating multiple QR codes.</li> + <li><strong>❌ No expiration dates:</strong> Cannot set codes to stop working after a certain date.</li> + <li><strong>❌ No password protection:</strong> Anyone with the QR code can access the content.</li> + </ul> + </div> + + <h3>Visual Example: Static QR Code Data Flow</h3> + <pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto"> +Static QR Code Content: +https://www.example.com/products/widget-a?ref=print-ad-2025 + +User Scans QR Code + ↓ +QR Scanner Decodes Pattern + ↓ +Opens: https://www.example.com/products/widget-a?ref=print-ad-2025 + ↓ +No Tracking | Cannot Edit | Works Forever + </pre> + + <h2>What is a Dynamic QR Code?</h2> + + <p>A dynamic QR code contains a short redirect URL instead of your actual content. This short URL points to a server that logs the scan data and then redirects to your final destination URL. The key advantage: you can change the destination URL anytime from your dashboard without reprinting the QR code.</p> + + <h3>How Dynamic QR Codes Work</h3> + <p>Think of a dynamic QR code like a phone forwarding service. When someone calls your forwarding number (the short URL in the QR code), the service logs the call and forwards it to your real phone (the destination URL). You can change your real phone number anytime without changing the forwarding number people dial.</p> + + <p><strong>Example:</strong> A dynamic QR code might contain <code>qrmaster.net/abc123</code>. When scanned, this redirects to your actual URL: <code>https://www.yourwebsite.com/summer-sale-2025</code>. Later, you can change it to <code>https://www.yourwebsite.com/fall-sale-2025</code> without reprinting.</p> + + <h3>Common Uses for Dynamic QR Codes</h3> + <ul> + <li><strong>Marketing campaigns:</strong> Print ads, billboards, posters where offers change</li> + <li><strong>Product packaging:</strong> Link to manuals that get updated</li> + <li><strong>Event tickets:</strong> Event details that might change</li> + <li><strong>Business cards:</strong> Update your website or portfolio without reprinting cards</li> + <li><strong>Restaurant menus:</strong> Daily specials and seasonal menu updates</li> + <li><strong>Retail displays:</strong> Promotions that change weekly or monthly</li> + </ul> + + <h3>Advantages of Dynamic QR Codes</h3> + <div class="bg-green-50 border-l-4 border-green-500 p-6 my-6 rounded-r-lg"> + <ul> + <li><strong>✅ Edit destination anytime:</strong> Change URL without reprinting QR codes—save thousands in reprint costs.</li> + <li><strong>✅ Full analytics:</strong> Track scans, geographic location, device types, time patterns, and user behavior.</li> + <li><strong>✅ A/B testing:</strong> Test different landing pages to optimize conversion rates.</li> + <li><strong>✅ Short, clean URLs:</strong> QR code contains <code>qrmaster.net/abc123</code> instead of long ugly URLs.</li> + <li><strong>✅ Set expiration dates:</strong> Configure codes to stop working after campaigns end.</li> + <li><strong>✅ Password protection:</strong> Require password to access destination content.</li> + <li><strong>✅ Retargeting pixels:</strong> Add Facebook Pixel, Google Ads tracking for remarketing.</li> + <li><strong>✅ Scheduled redirects:</strong> Change destination based on time/date automatically.</li> + </ul> + </div> + + <h3>Disadvantages of Dynamic QR Codes</h3> + <div class="bg-red-50 border-l-4 border-red-500 p-6 my-6 rounded-r-lg"> + <ul> + <li><strong>❌ Depends on service provider:</strong> If the service shuts down or your subscription lapses, QR codes stop working.</li> + <li><strong>❌ Subscription required:</strong> Most services charge $5-50/month for advanced features (though QR Master Free offers 3 codes free).</li> + <li><strong>❌ Slight redirect delay:</strong> Extra 50-200ms for server redirect (usually imperceptible).</li> + <li><strong>❌ Requires internet:</strong> Cannot work offline (though the destination can be cached).</li> + <li><strong>❌ Privacy concerns:</strong> Service provider can see scan data (choose GDPR-compliant providers).</li> + </ul> + </div> + + <h3>Visual Example: Dynamic QR Code Data Flow</h3> + <pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto"> +Dynamic QR Code Content: +qrmaster.net/abc123 + +User Scans QR Code + ↓ +QR Scanner Decodes Pattern + ↓ +Contacts: qrmaster.net/abc123 + ↓ +Server Logs: Device, Location, Time, User Agent + ↓ +Redirects to: https://www.example.com/current-promotion + ↓ +Tracking ✓ | Editable ✓ | Analytics ✓ + </pre> + + <div class="my-8"> + <img src="/blog/2-body.png" alt="Business card with elegant QR code" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>Static vs Dynamic QR Codes: Side-by-Side Comparison</h2> + + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Feature</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Static QR Code</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Dynamic QR Code</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Edit After Printing</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Track Scans</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Analytics Dashboard</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">A/B Testing</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">URL Length in QR</td> + <td class="border border-gray-300 px-6 py-4">Long (full URL)</td> + <td class="border border-gray-300 px-6 py-4">Short (redirect URL)</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Cost</td> + <td class="border border-gray-300 px-6 py-4">Free (forever)</td> + <td class="border border-gray-300 px-6 py-4">Free - $50/month</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Works Forever</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + <td class="border border-gray-300 px-6 py-4">Depends on subscription</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Redirect Speed</td> + <td class="border border-gray-300 px-6 py-4">Instant</td> + <td class="border border-gray-300 px-6 py-4">50-200ms delay</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Privacy</td> + <td class="border border-gray-300 px-6 py-4">High (no tracking)</td> + <td class="border border-gray-300 px-6 py-4">Lower (tracked)</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Expiration Date</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Password Protection</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Yes</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Best For</td> + <td class="border border-gray-300 px-6 py-4">Fixed, permanent content</td> + <td class="border border-gray-300 px-6 py-4">Marketing campaigns</td> + </tr> + </tbody> + </table> + </div> + + <h2>Real-World Scenarios: Static vs Dynamic</h2> + + <h3>When Static QR Codes Excel</h3> + + <h4>Scenario 1: Business Card Contact Information</h4> + <p><strong>Situation:</strong> Professional wants to share their contact details easily at networking events.</p> + <p><strong>Why Static:</strong> Contact information rarely changes. No tracking needed for personal cards. QR code works forever even if you stop paying for QR service. Privacy-friendly (no data collection).</p> + <p><strong>Cost Savings:</strong> $0 forever vs $5-15/month for dynamic QR service = $60-180/year saved.</p> + + <h4>Scenario 2: Book Back Cover</h4> + <p><strong>Situation:</strong> Author wants QR code on book cover linking to their website.</p> + <p><strong>Why Static:</strong> Author's website URL is unlikely to change. Book will be in print for years. No need to track individual reader scans. Permanent, reliable link.</p> + + <h4>Scenario 3: Memorial Plaque</h4> + <p><strong>Situation:</strong> Memorial plaque with QR code linking to person's biography.</p> + <p><strong>Why Static:</strong> Content is permanent. No tracking appropriate for memorial. Must work for decades without depending on subscription service. Ultimate longevity required.</p> + + <h3>When Dynamic QR Codes Are Essential</h3> + + <h4>Scenario 1: Billboard Advertising Campaign</h4> + <p><strong>Situation:</strong> Company runs 3-month billboard campaign with seasonal offer.</p> + <p><strong>Why Dynamic:</strong> Offer changes monthly. Must track which billboard locations perform best. ROI calculation requires scan data. A/B test different landing pages. After campaign ends, redirect to general website instead of expired offer.</p> + <p><strong>Cost Justification:</strong> Billboard costs $5,000/month. QR service costs $9/month. Tracking data helps optimize $15,000 campaign spend—easily worth $27 for analytics.</p> + + <h4>Scenario 2: Product Packaging (10,000 Units)</h4> + <p><strong>Situation:</strong> Manufacturing 10,000 product boxes with QR code to user manual PDF.</p> + <p><strong>Why Dynamic:</strong> Manual might get updated (typo corrections, new features, safety warnings). Cannot recall 10,000 products if URL changes. Need to track which regions/stores have highest engagement. Reprint cost is $5,000+ vs $9/month dynamic QR service.</p> + + <h4>Scenario 3: Business Cards for Consultant</h4> + <p><strong>Situation:</strong> Printing 500 business cards with QR code to portfolio.</p> + <p><strong>Why Dynamic:</strong> Portfolio website URL might change (rebranding, new domain). Can track which networking events drive most scans. Update QR to point to specific landing page for each prospect. Add new projects without reprinting cards.</p> + + <h2>When to Use Static QR Codes</h2> + + <p>Choose static QR codes when:</p> + + <h3>1. Content Never Changes</h3> + <ul> + <li>Contact information (vCard) that remains constant</li> + <li>App store download link (Apple App Store / Google Play URLs are stable)</li> + <li>Company homepage that's been the same for years</li> + <li>Historical information (museum exhibits, memorial plaques)</li> + </ul> + + <h3>2. Privacy is Critical</h3> + <ul> + <li>Personal contact information (vCard)</li> + <li>Sensitive documents where tracking is inappropriate</li> + <li>Legal/compliance scenarios where data collection is restricted</li> + <li>Medical information (HIPAA compliance concerns)</li> + </ul> + + <h3>3. Long-Term Reliability Needed</h3> + <ul> + <li>Museum exhibits (must work for decades)</li> + <li>Book publications (no ongoing subscription acceptable)</li> + <li>Historical markers or public art installations</li> + <li>Gravestones/memorial markers (ultimate permanence)</li> + </ul> + + <h3>4. Offline Content</h3> + <ul> + <li>vCard contact information (stored locally on device)</li> + <li>Location coordinates (opens maps app directly)</li> + <li>Plain text messages or instructions</li> + <li>SMS or phone number links</li> + </ul> + + <h3>5. Budget is $0</h3> + <ul> + <li>Personal projects with no funding</li> + <li>Small nonprofits with zero marketing budget</li> + <li>One-time events with no tracking needs</li> + <li>Side projects and hobby uses</li> + </ul> + + <h2>When to Use Dynamic QR Codes</h2> + + <p>Choose dynamic QR codes when:</p> + + <h3>1. Content Might Change</h3> + <ul> + <li>Seasonal promotions (summer sale → fall sale)</li> + <li>Product manuals that get updated</li> + <li>Event schedules or venue information</li> + <li>Restaurant menus with changing prices/items</li> + <li>Portfolio or resume links that evolve</li> + </ul> + + <h3>2. Tracking is Important</h3> + <ul> + <li>Marketing campaigns (need to measure ROI)</li> + <li>Product engagement (which products get scanned most?)</li> + <li>Event attendance (check-in tracking)</li> + <li>Print advertising (cost per scan analysis)</li> + <li>Geographic targeting (where are scans coming from?)</li> + </ul> + + <h3>3. A/B Testing Needed</h3> + <ul> + <li>Test different landing pages without reprinting</li> + <li>Optimize offers based on scan conversion data</li> + <li>Compare two different CTAs (call-to-action messages)</li> + <li>Experiment with different promotional strategies</li> + </ul> + + <h3>4. Large Print Run</h3> + <ul> + <li>50,000+ product labels (cannot afford reprint if URL changes)</li> + <li>10,000+ event posters (investment protection)</li> + <li>1,000+ business cards (future-proofing)</li> + <li>Any quantity where reprint cost > dynamic QR subscription cost</li> + </ul> + + <h3>5. Professional Marketing</h3> + <ul> + <li>Billboard advertising (high cost requires tracking)</li> + <li>Magazine ads (attribution to specific publications)</li> + <li>Product packaging (ongoing engagement measurement)</li> + <li>Retail displays (optimize based on performance data)</li> + <li>Direct mail campaigns (response rate tracking)</li> + </ul> + + <h2>Cost Analysis: Static vs Dynamic</h2> + + <h3>Static QR Code Total Cost</h3> + <p><strong>Creation:</strong> $0<br> + <strong>Maintenance:</strong> $0/month<br> + <strong>Lifetime Cost (10 years):</strong> $0</p> + + <p><strong>Risk:</strong> If URL changes, must reprint all materials. For 10,000 business cards: $200-500 reprint cost.</p> + + <h3>Dynamic QR Code Total Cost</h3> + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Provider</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Free Tier</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Pro Tier</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Enterprise</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Master</td> + <td class="border border-gray-300 px-6 py-4">3 codes free</td> + <td class="border border-gray-300 px-6 py-4">$9/mo (50 codes)</td> + <td class="border border-gray-300 px-6 py-4">$29/mo (500 codes)</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Code Generator</td> + <td class="border border-gray-300 px-6 py-4">1 code (trial)</td> + <td class="border border-gray-300 px-6 py-4">$12/mo</td> + <td class="border border-gray-300 px-6 py-4">$50/mo</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">Beaconstac</td> + <td class="border border-gray-300 px-6 py-4">Trial only</td> + <td class="border border-gray-300 px-6 py-4">$49/mo</td> + <td class="border border-gray-300 px-6 py-4">$199/mo</td> + </tr> + </tbody> + </table> + </div> + + <h3>Break-Even Analysis: 10,000 Business Cards</h3> + + <p><strong>Option A: Static QR Code</strong></p> + <ul> + <li>Print cost: $200</li> + <li>QR code service: $0</li> + <li>Total: $200</li> + <li>Risk: If URL changes, reprint costs another $200</li> + </ul> + + <p><strong>Option B: Dynamic QR Code</strong></p> + <ul> + <li>Print cost: $200</li> + <li>QR service: $9/month × 12 months = $108/year</li> + <li>Total Year 1: $308</li> + <li>Benefit: Update URL anytime, track all scans, optimize campaigns</li> + </ul> + + <p><strong>Conclusion:</strong> If there's ANY chance the URL might change, dynamic QR saves money. Reprint cost ($200) > annual dynamic service cost ($108).</p> + + <h3>Cost Recommendation by Print Quantity</h3> + <ul> + <li><strong>1-100 prints:</strong> Static is fine (low reprint cost)</li> + <li><strong>100-1,000 prints:</strong> Dynamic if content might change or tracking needed</li> + <li><strong>1,000+ prints:</strong> Always use dynamic (reprint cost too high to risk)</li> + </ul> + + <h2>Can You Convert Static to Dynamic?</h2> + + <p><strong>Short answer: No.</strong></p> + + <p>Once a static QR code is created and printed, you cannot convert it to dynamic. The data is permanently encoded in the QR pattern. The only solution is to reprint with a new dynamic QR code.</p> + + <h3>Migration Strategy</h3> + <p>If you have existing static QR codes that need updating:</p> + + <ol> + <li><strong>Domain redirect:</strong> If you control the domain, set up a server-side redirect from the static URL to the new destination.</li> + <li><strong>Phased replacement:</strong> Gradually replace static codes with dynamic as you reprint materials.</li> + <li><strong>Reprint high-value materials first:</strong> Billboard ads and product packaging first; business cards later.</li> + </ol> + + <h3>Prevention: Plan Ahead</h3> + <p>If there's ANY chance you'll need to edit the URL in the future:</p> + <ul> + <li>Start with dynamic QR codes from day one</li> + <li>Cost is minimal ($9-29/month) compared to reprint expenses</li> + <li>Better to have the flexibility and not need it than need it and not have it</li> + </ul> + + <h2>Special Cases: Wikipedia and QRpedia Example</h2> + + <p>An interesting real-world example of dynamic QR codes is <a href="https://meta.wikimedia.org/wiki/QRpedia" target="_blank" rel="noopener">QRpedia</a>, a system created by Wikipedia. QRpedia generates QR codes that link to Wikipedia articles. When scanned, the system:</p> + + <ol> + <li>Detects the scanner's language preference</li> + <li>Redirects to the Wikipedia article in that language</li> + <li>Falls back to English if the article doesn't exist in the user's language</li> + </ol> + + <p>This is only possible with dynamic QR codes. A static QR code would have to link to a single language version, reducing accessibility for international visitors. QRpedia demonstrates how dynamic QR codes enable sophisticated, user-personalized experiences.</p> + + <h2>Decision Tree: Static or Dynamic?</h2> + + <pre class="bg-gray-100 p-6 rounded-lg my-6 overflow-x-auto"> +Will the destination URL ever change? + ├─> YES → Use Dynamic QR Code + └─> NO → Do you need scan tracking/analytics? + ├─> YES → Use Dynamic QR Code + └─> NO → Is budget absolutely $0? + ├─> YES → Use Static QR Code + └─> NO → Consider Dynamic for future flexibility + </pre> + + <h2>Best Practices: Choosing Static vs Dynamic</h2> + + <h3>1. Default to Dynamic for Business Use</h3> + <p>When in doubt, choose dynamic. The cost is minimal ($9-29/month), and the flexibility protects your investment. Even if you don't think you'll need to change the URL, business circumstances change—mergers, rebrands, website migrations all happen.</p> + + <h3>2. Use Static for Personal, Permanent Content</h3> + <p>Personal projects, hobby websites, memorial content, and truly permanent information can safely use static QR codes. No ongoing cost, works forever, privacy-friendly.</p> + + <h3>3. Calculate the Reprint Cost</h3> + <p>Before deciding, calculate: "What would it cost to reprint if I'm wrong?" If reprint cost > 2 years of dynamic service, go dynamic.</p> + + <h3>4. Consider the Print Quantity</h3> + <ul> + <li>1-10 prints: Static is fine</li> + <li>10-100 prints: Consider dynamic if content might change</li> + <li>100-1,000 prints: Probably dynamic</li> + <li>1,000+ prints: Definitely dynamic</li> + </ul> + + <h3>5. Think About the Time Horizon</h3> + <ul> + <li>Short-term campaign (1-6 months): Dynamic essential</li> + <li>Medium-term (6 months - 2 years): Dynamic recommended</li> + <li>Long-term (2-5 years): Dynamic unless content truly permanent</li> + <li>Permanent (5+ years): Static acceptable if content won't change</li> + </ul> + + <h2>Conclusion</h2> + + <p>The choice between static and dynamic QR codes fundamentally comes down to two questions:</p> + + <ol> + <li><strong>Will the destination ever need to change?</strong></li> + <li><strong>Do you need scan tracking and analytics?</strong></li> + </ol> + + <p>If you answered "yes" to either question, dynamic QR codes are the clear choice. For truly permanent content with zero tracking needs and zero budget, static QR codes remain a perfectly valid option.</p> + + <p>Most businesses will benefit from dynamic QR codes. The ability to update destinations, track campaign performance, and A/B test landing pages provides enormous value. At $9-29/month, the cost is negligible compared to the flexibility and insights gained.</p> + + <p>Start with QR Master's free plan (3 dynamic codes) to test the technology. Upgrade when you need more codes or advanced features. Future-proof your QR strategy with dynamic codes, and never worry about reprint costs again.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Ready to Create Dynamic QR Codes?</h3> + <p class="text-lg text-gray-700 mb-6">Start with 3 free dynamic QR codes. No credit card required. Full analytics dashboard included.</p> + <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Get Started Free →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a> - Create editable, trackable QR codes</li> + <li><a href="/blog/qr-code-tracking-guide-2025">QR Code Tracking Guide</a> - Learn how to track scans</li> + <li><a href="/blog/bulk-qr-code-generator-excel">Bulk QR Generator from Excel</a> - Generate hundreds at once</li> + <li><a href="/pricing">Pricing Plans</a> - Compare free and paid options</li> + <li><a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">QR Code on Wikipedia</a> - Technical standards (ISO/IEC 18004)</li> + <li><a href="https://meta.wikimedia.org/wiki/QRpedia" target="_blank" rel="noopener">QRpedia on Wikipedia</a> - Real-world dynamic QR example</li> + </ul> + </div>`, + }, + 'bulk-qr-code-generator-excel': { + slug: 'bulk-qr-code-generator-excel', + title: 'How to Generate Bulk QR Codes from Excel: Complete Tutorial 2025', + excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools. Perfect for products, events, inventory.', + date: 'October 16, 2025', + datePublished: '2025-10-16T10:00:00Z', + dateModified: '2025-10-16T10:00:00Z', + readTime: '13 Min', + category: 'Bulk Generation', + image: '/blog/3-hero.png', + imageAlt: 'Multiple QR codes arranged in organized grid pattern', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'Bulk QR code generation from Excel allows you to create hundreds or thousands of QR codes simultaneously by uploading a CSV or Excel file. The file should contain columns for name, URL, and optional metadata. Tools like QR Master Pro can process 1,000+ codes in minutes, saving hours of manual work. Perfect for product labels, event tickets, asset tracking, and marketing campaigns.', + howTo: { + name: 'How to Generate Bulk QR Codes from Excel', + description: 'Step-by-step tutorial for creating multiple QR codes from Excel or CSV files', + totalTime: 'PT10M', + steps: [ + { + name: 'Prepare Your Excel File', + text: 'Create an Excel or CSV file with columns: name, url, description, and tags. Fill in your data with one QR code per row.', + }, + { + name: 'Sign Up for QR Master Business', + text: 'Create a QR Master account and upgrade to Business plan for bulk upload feature (supports up to 500 codes).', + url: 'https://www.qrmaster.net/signup', + }, + { + name: 'Upload Your File', + text: 'Navigate to Create → Bulk Upload and drag-drop your Excel/CSV file. The system will auto-detect columns.', + }, + { + name: 'Map Columns and Customize', + text: 'Verify column mapping is correct. Optionally customize QR design: add logo, set colors, choose frame style.', + }, + { + name: 'Generate and Download', + text: 'Click Generate All. Processing takes 2-4 minutes for 1,000 codes. Download the ZIP file with all QR codes organized by name.', + url: 'https://www.qrmaster.net/bulk-qr-code-generator', + }, + ], + }, + content: `<div class="blog-content"> + <p>Need to create QR codes for 100, 500, or even 1,000 products? Manual generation would take hours—but bulk QR code generation from Excel or CSV files can complete the job in minutes. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented for tracking automotive parts in bulk, and today's batch generation tools continue that efficiency for modern applications.</p> + + <p>This comprehensive guide shows you exactly how to generate bulk QR codes from Excel, including file format requirements, step-by-step tutorials, use cases, and tool comparisons. Perfect for e-commerce, events, inventory management, and marketing campaigns.</p> + + <h2>What is Bulk QR Code Generation?</h2> + + <p>Bulk QR code generation is the process of creating multiple QR codes simultaneously from a data file (Excel or CSV). Instead of manually entering data for each QR code one-by-one, you upload a spreadsheet containing all your data, and the system generates all QR codes automatically.</p> + + <h3>Time Savings Comparison</h3> + <p><strong>Manual Creation:</strong> 2-5 minutes per QR code</p> + <ul> + <li>100 codes = 3-8 hours of repetitive work</li> + <li>500 codes = 16-40 hours (2-5 full work days!)</li> + <li>1,000 codes = 33-83 hours</li> + </ul> + + <p><strong>Bulk Creation from Excel:</strong> 2-3 minutes total</p> + <ul> + <li>100 codes = 2 minutes</li> + <li>500 codes = 2-3 minutes</li> + <li>1,000 codes = 3-4 minutes</li> + </ul> + + <p><strong>Time Saved:</strong> For 500 QR codes, bulk generation saves approximately 16-40 hours of work. That's nearly a full work week of productivity gained.</p> + + <h3>Common Use Cases for Bulk QR Generation</h3> + <ul> + <li><strong>Product Labels:</strong> Generate QR code for each SKU linking to product manual, warranty, or reviews</li> + <li><strong>Event Tickets:</strong> Create unique QR codes for each attendee for check-in and access control</li> + <li><strong>Asset Management:</strong> Track office equipment, IT hardware, or inventory with QR stickers</li> + <li><strong>Marketing Campaigns:</strong> Multiple store locations each get unique QR code for tracking</li> + <li><strong>Restaurant Menus:</strong> Different QR codes for each dish or table</li> + <li><strong>Real Estate:</strong> Unique QR code for each property listing</li> + <li><strong>Business Cards:</strong> Generate personalized QR codes for each team member</li> + </ul> + + <h2>How Bulk QR Generation Works</h2> + + <pre class="bg-gray-100 p-6 rounded-lg my-6 overflow-x-auto"> +Step 1: Prepare Data + ↓ +Excel/CSV File: +Product Name | URL | SKU +Product A | https://manual.com/product-a | 001 +Product B | https://manual.com/product-b | 002 +Product C | https://manual.com/product-c | 003 + +Step 2: Upload to QR Generator + ↓ +Map Columns: +‱ Name → QR Code Title +‱ URL → Destination URL +‱ SKU → File Name + +Step 3: Customize Design (Optional) + ↓ +Apply Branding to ALL Codes: +‱ Upload Logo +‱ Set Brand Colors +‱ Choose Frame Style +‱ Set Image Size + +Step 4: Generate & Download + ↓ +Download ZIP File: +📩 qr-codes.zip + ├─ product-001.png + ├─ product-002.png + ├─ product-003.png + └─ ... (all codes) + </pre> + + <h3>System Requirements</h3> + <ul> + <li><strong>File Format:</strong> Excel (.xlsx) or CSV (.csv)</li> + <li><strong>Minimum Columns:</strong> Name and URL (required)</li> + <li><strong>Optional Columns:</strong> Description, Tags, Category, Custom Fields</li> + <li><strong>Max File Size:</strong> Usually 10-50MB depending on service</li> + <li><strong>Max Rows:</strong> 1,000-10,000 depending on service tier</li> + </ul> + + <h3>Output Format</h3> + <ul> + <li><strong>File Type:</strong> ZIP archive containing individual QR code images</li> + <li><strong>Image Format:</strong> PNG (most common), SVG, or PDF</li> + <li><strong>Resolution:</strong> 200x200px (web), 500x500px (print), 1000x1000px (high-res)</li> + <li><strong>File Naming:</strong> Based on your chosen column (SKU, name, or custom)</li> + <li><strong>Organization:</strong> All codes in one folder, optionally organized by category</li> + </ul> + + <h2>Excel File Format & Template</h2> + + <h3>Required Columns</h3> + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Column Name</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Description</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Required</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Example</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">name</td> + <td class="border border-gray-300 px-6 py-4">QR code title/label</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">Yes</td> + <td class="border border-gray-300 px-6 py-4">Summer Promo Flyer</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">url</td> + <td class="border border-gray-300 px-6 py-4">Destination URL</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">Yes</td> + <td class="border border-gray-300 px-6 py-4">https://example.com/sale</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">description</td> + <td class="border border-gray-300 px-6 py-4">Optional notes</td> + <td class="border border-gray-300 px-6 py-4 text-gray-600">No</td> + <td class="border border-gray-300 px-6 py-4">50% off summer sale</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">tags</td> + <td class="border border-gray-300 px-6 py-4">Categories (comma-separated)</td> + <td class="border border-gray-300 px-6 py-4 text-gray-600">No</td> + <td class="border border-gray-300 px-6 py-4">marketing, summer, 2025</td> + </tr> + </tbody> + </table> + </div> + + <h3>Example Excel File Content</h3> + <pre class="bg-gray-100 p-4 rounded-lg my-6 overflow-x-auto"> +name,url,description,tags +Product A Manual,https://manuals.com/product-a,User manual for Product A,manuals electronics +Product B Warranty,https://warranty.com/product-b,Warranty registration,warranty electronics +Store Location NYC,https://maps.com/store-nyc,NYC store directions,locations stores +Store Location LA,https://maps.com/store-la,LA store directions,locations stores +Event Ticket 001,https://checkin.com/verify/001,VIP ticket,events tickets +Event Ticket 002,https://checkin.com/verify/002,General admission,events tickets + </pre> + + <h3>Best Practices for File Preparation</h3> + + <h4>1. Clean Your Data</h4> + <ul> + <li><strong>Remove empty rows:</strong> Delete any rows with missing required fields</li> + <li><strong>Validate URLs:</strong> Ensure all URLs start with https:// or http://</li> + <li><strong>No special characters in filenames:</strong> Avoid / \ : * ? " < > | in name column</li> + <li><strong>Consistent naming:</strong> Use a standardized naming scheme (e.g., PROD-001, PROD-002)</li> + <li><strong>Check for duplicates:</strong> Remove duplicate URLs or names if not intended</li> + </ul> + + <h4>2. Test with Small Batch First</h4> + <ul> + <li>Upload only 5-10 rows initially</li> + <li>Verify output is correct (check 2-3 QR codes)</li> + <li>Confirm file naming and organization matches expectations</li> + <li>Then upload your full dataset</li> + </ul> + + <h4>3. URL Formatting</h4> + <ul> + <li><strong>Include protocol:</strong> Always use <code>https://example.com</code> not <code>example.com</code></li> + <li><strong>Test all URLs:</strong> Click each link to verify it works</li> + <li><strong>Use URL shorteners if needed:</strong> Shorter URLs = simpler QR codes</li> + <li><strong>Avoid special characters:</strong> URL-encode spaces and special characters</li> + </ul> + + <h4>4. Smart File Naming</h4> + <ul> + <li>Use SKU or product ID in name column: "PROD-001", "PROD-002"</li> + <li>Keeps downloaded files organized and easy to identify</li> + <li>Matches physical inventory labels</li> + <li>Enables easy search and sorting</li> + </ul> + + <h4>5. File Size Management</h4> + <ul> + <li><strong>Keep under 10MB:</strong> Most services have file size limits</li> + <li><strong>Split large datasets:</strong> Divide 5,000 rows into 5 files of 1,000 each</li> + <li><strong>Typical row size:</strong> 1,000 rows ≈ 50-100KB</li> + <li><strong>Remove unnecessary columns:</strong> Only include columns you need</li> + </ul> + + <div class="my-8"> + <img src="/blog/3-body.png" alt="Person working at desk with laptop and QR code materials" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>Step-by-Step Tutorial with QR Master</h2> + + <h3>Step 1: Prepare Your Excel File</h3> + <ol> + <li>Open Excel, Google Sheets, or any spreadsheet app</li> + <li>Create columns: <code>name</code>, <code>url</code>, <code>description</code>, <code>tags</code></li> + <li>Fill in your data (one QR code per row)</li> + <li>Example: + <pre class="bg-gray-100 p-4 rounded-lg my-4"> +name | url | tags +Product A | https://shop.com/product-a | electronics, sale +Product B | https://shop.com/product-b | electronics +Event Ticket 1 | https://event.com/ticket/1 | events, tickets + </pre> + </li> + <li>Save as <code>.xlsx</code> or export as <code>.csv</code></li> + </ol> + + <h3>Step 2: Sign Up for QR Master</h3> + <ol> + <li>Go to <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li> + <li>Create free account (email + password)</li> + <li>Verify your email</li> + <li><strong>Free plan:</strong> Up to 3 dynamic QR codes (no bulk upload)</li> + <li><strong>Business plan:</strong> $29/month, up to 500 codes, bulk upload feature ✅</li> + </ol> + + <h3>Step 3: Navigate to Bulk Upload</h3> + <ol> + <li>Log into your QR Master dashboard</li> + <li>Click <strong>"Create QR Code"</strong> button</li> + <li>Select <strong>"Bulk Upload"</strong> tab</li> + <li>Choose <strong>"Upload Excel/CSV"</strong></li> + <li>Or drag and drop your file directly</li> + </ol> + + <h3>Step 4: Map Your Columns</h3> + <ol> + <li>System auto-detects column names</li> + <li>Verify mapping is correct: + <ul> + <li><code>name</code> → QR Code Title</li> + <li><code>url</code> → Destination URL</li> + <li><code>description</code> → Description</li> + <li><code>tags</code> → Tags</li> + </ul> + </li> + <li>Preview shows first 5 rows</li> + <li>Check data looks correct</li> + <li>Click <strong>"Looks Good"</strong> to proceed</li> + </ol> + + <h3>Step 5: Customize Design (Optional)</h3> + <p>Apply branding to ALL QR codes simultaneously:</p> + + <h4>Upload Logo</h4> + <ul> + <li>Click "Upload Logo"</li> + <li>Select PNG or SVG (max 1MB)</li> + <li>Logo appears in center of all QR codes</li> + <li>Recommended: Square logo, transparent background</li> + </ul> + + <h4>Set Colors</h4> + <ul> + <li><strong>Foreground:</strong> QR code pattern color (default: #000000 black)</li> + <li><strong>Background:</strong> QR code background (default: #FFFFFF white)</li> + <li><strong>Use brand colors:</strong> e.g., #FF6B6B for foreground, #FFFFFF for background</li> + <li><strong>Ensure contrast:</strong> Dark foreground + light background for scannability</li> + </ul> + + <h4>Choose Frame Style</h4> + <ul> + <li><strong>No frame:</strong> Plain QR code only</li> + <li><strong>Square frame:</strong> Professional border</li> + <li><strong>Rounded frame:</strong> Modern, friendly look</li> + <li><strong>With text:</strong> Add "Scan Me" or custom CTA text</li> + </ul> + + <h4>Set Image Size</h4> + <ul> + <li><strong>200x200px:</strong> Web use, social media</li> + <li><strong>500x500px:</strong> Standard print (business cards, flyers)</li> + <li><strong>1000x1000px:</strong> High-resolution print (posters, banners)</li> + <li><strong>2000x2000px:</strong> Billboard, large-format print</li> + </ul> + + <h3>Step 6: Generate QR Codes</h3> + <ol> + <li>Click <strong>"Generate All"</strong> button</li> + <li>System begins processing: + <ul> + <li>100 codes ≈ 30 seconds</li> + <li>500 codes ≈ 2 minutes</li> + <li>1,000 codes ≈ 4 minutes</li> + </ul> + </li> + <li>Progress bar shows real-time status</li> + <li>Email notification when complete (for large batches)</li> + <li>Do not close browser window while processing</li> + </ol> + + <h3>Step 7: Download & Use</h3> + <ol> + <li>Click <strong>"Download ZIP"</strong> button</li> + <li>ZIP file downloads to your computer</li> + <li>Extract the archive: + <ul> + <li>Windows: Right-click → Extract All</li> + <li>Mac: Double-click ZIP file</li> + </ul> + </li> + <li>Files are named using your <code>name</code> column</li> + <li>Example: <code>product-001.png</code>, <code>product-002.png</code></li> + <li>Organized and ready to use immediately</li> + </ol> + + <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> + <h3 class="text-xl font-semibold mb-2 text-gray-900">Pro Tip: CSV Export from Google Sheets</h3> + <p class="text-gray-800">Google Sheets users: File → Download → Comma-separated values (.csv). This format works perfectly with all QR code generators and is often smaller than Excel files.</p> + </div> + + <h2>Use Cases & Examples</h2> + + <h3>1. E-Commerce Product Labels</h3> + <p><strong>Scenario:</strong> Online store has 500 products, each needs QR code linking to:</p> + <ul> + <li>Product manual PDF</li> + <li>Warranty registration page</li> + <li>Customer support contact</li> + </ul> + + <p><strong>Excel Setup:</strong></p> + <pre class="bg-gray-100 p-4 rounded-lg my-4"> +name,url +SKU-001,https://manual.com/sku-001 +SKU-002,https://manual.com/sku-002 +SKU-003,https://manual.com/sku-003 +... + </pre> + + <p><strong>Result:</strong></p> + <ul> + <li>500 QR codes generated in 2 minutes</li> + <li>Print on product label stickers</li> + <li>Stick on packaging before shipping</li> + <li>Track which products get most support requests via scan analytics</li> + </ul> + + <p><strong>Time Saved:</strong> 500 codes × 3 min/code = 25 hours saved!</p> + + <h3>2. Event Tickets (1,000 Attendees)</h3> + <p><strong>Scenario:</strong> Conference with 1,000 attendees, each needs unique QR code for:</p> + <ul> + <li>Check-in at venue</li> + <li>Session access verification</li> + <li>Prevent duplicate entries</li> + </ul> + + <p><strong>Excel Setup:</strong></p> + <pre class="bg-gray-100 p-4 rounded-lg my-4"> +name,url,description +Ticket-001,https://checkin.com/verify/001,John Doe - VIP +Ticket-002,https://checkin.com/verify/002,Jane Smith - General +Ticket-003,https://checkin.com/verify/003,Bob Johnson - Speaker +... + </pre> + + <p><strong>Result:</strong></p> + <ul> + <li>Unique QR per ticket (prevents sharing)</li> + <li>Real-time check-in tracking</li> + <li>Instant attendance reports</li> + <li>Session-specific access control</li> + </ul> + + <div class="my-8"> + <img src="https://images.unsplash.com/photo-1505373877841-8d25f7d46678?w=800&q=80" alt="Event tickets with unique QR codes" class="rounded-lg shadow-lg w-full" /> + </div> + + <h3>3. Asset Management (200 Office Items)</h3> + <p><strong>Scenario:</strong> IT department needs to track office equipment:</p> + <ul> + <li>Laptops</li> + <li>Monitors</li> + <li>Desks and chairs</li> + <li>Printers</li> + </ul> + + <p><strong>Excel Setup:</strong></p> + <pre class="bg-gray-100 p-4 rounded-lg my-4"> +name,url,description +LAPTOP-001,https://assets.com/laptop-001,Dell Latitude 5420 +LAPTOP-002,https://assets.com/laptop-002,MacBook Pro 14" +DESK-001,https://assets.com/desk-001,Standing Desk - Office 3A +PRINTER-001,https://assets.com/printer-001,HP LaserJet Pro +... + </pre> + + <p><strong>Result:</strong></p> + <ul> + <li>QR code sticker on each item</li> + <li>Scan to view: Current owner, purchase date, warranty, maintenance history</li> + <li>Update info dynamically (no sticker replacement needed)</li> + <li>Easy inventory audits</li> + </ul> + + <h3>4. Multi-Location Marketing (50 Stores)</h3> + <p><strong>Scenario:</strong> Retail chain with 50 locations, each gets unique QR code for:</p> + <ul> + <li>Local promotions</li> + <li>Store-specific tracking</li> + <li>Regional offers</li> + </ul> + + <p><strong>Excel Setup:</strong></p> + <pre class="bg-gray-100 p-4 rounded-lg my-4"> +name,url,tags +NYC-Store,https://promo.com?location=nyc,new-york retail +LA-Store,https://promo.com?location=la,california retail +Chicago-Store,https://promo.com?location=chicago,illinois retail +... + </pre> + + <p><strong>Result:</strong></p> + <ul> + <li>Track which stores drive most QR scans</li> + <li>Different promotions per location</li> + <li>Measure local campaign ROI</li> + <li>Optimize regional marketing spend</li> + </ul> + + <h2>Free vs Paid Bulk QR Tools</h2> + + <h3>Free Tools</h3> + + <h4>1. QR Master Free</h4> + <ul> + <li><strong>Limit:</strong> 3 dynamic codes (no bulk upload feature)</li> + <li><strong>Best for:</strong> Testing the platform before upgrading</li> + <li><strong>Upgrade Path:</strong> $29/mo Business plan for bulk + 500 codes</li> + </ul> + + <h4>2. QuickChart (API-Based)</h4> + <ul> + <li><strong>Type:</strong> Open-source API</li> + <li><strong>Free:</strong> Yes, unlimited</li> + <li><strong>Bulk:</strong> Requires coding (Python, JavaScript, etc.)</li> + <li><strong>Best for:</strong> Developers comfortable with APIs</li> + </ul> + + <h4>3. Google Sheets + Apps Script</h4> + <ul> + <li><strong>Limit:</strong> 100 codes per execution (can run multiple times)</li> + <li><strong>Free:</strong> Yes</li> + <li><strong>Bulk:</strong> Requires Google Apps Script knowledge</li> + <li><strong>Complexity:</strong> Moderate technical skill required</li> + </ul> + + <h3>Paid Tools (Recommended for Business)</h3> + + <div class="overflow-x-auto my-8"> + <table class="min-w-full border-collapse border border-gray-300"> + <thead class="bg-gray-100"> + <tr> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Tool</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Price</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Max Codes</th> + <th class="border border-gray-300 px-6 py-3 text-left font-semibold">Bulk Upload</th> + </tr> + </thead> + <tbody> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Pro</td> + <td class="border border-gray-300 px-6 py-4">$9/mo</td> + <td class="border border-gray-300 px-6 py-4">50 codes</td> + <td class="border border-gray-300 px-6 py-4 text-red-600">❌ No</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Master Business</td> + <td class="border border-gray-300 px-6 py-4">$29/mo</td> + <td class="border border-gray-300 px-6 py-4">500 codes</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Excel/CSV</td> + </tr> + <tr class="bg-white"> + <td class="border border-gray-300 px-6 py-4 font-medium">QR Code Generator</td> + <td class="border border-gray-300 px-6 py-4">$50/mo</td> + <td class="border border-gray-300 px-6 py-4">Unlimited</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Excel/CSV</td> + </tr> + <tr class="bg-gray-50"> + <td class="border border-gray-300 px-6 py-4 font-medium">Beaconstac</td> + <td class="border border-gray-300 px-6 py-4">$99/mo</td> + <td class="border border-gray-300 px-6 py-4">500 codes</td> + <td class="border border-gray-300 px-6 py-4 text-green-600">✅ Excel/CSV</td> + </tr> + </tbody> + </table> + </div> + + <p><strong>Our Recommendation:</strong></p> + <ul> + <li><strong>For 1-50 codes:</strong> Manual creation or QR Master Pro</li> + <li><strong>For 50-500 codes:</strong> QR Master Business at $29/mo (best value)</li> + <li><strong>For 500+ codes:</strong> QR Master Business or enterprise custom quote</li> + <li><strong>For developers:</strong> QuickChart API (free, unlimited, requires coding)</li> + </ul> + + <h2>Advanced Tips & Tricks</h2> + + <h3>1. Use Dynamic QR Codes for Bulk Generation</h3> + <p>Always use dynamic QR codes for bulk generation (even though they cost more). Why?</p> + <ul> + <li><strong>Edit any URL later:</strong> If Product A manual URL changes, update it without reprinting 10,000 labels</li> + <li><strong>Track individual code performance:</strong> See which products get most scans</li> + <li><strong>Future-proof:</strong> Protect your investment in printed materials</li> + </ul> + + <h3>2. Organize with Tags and Categories</h3> + <p>Use the <code>tags</code> column strategically:</p> + <ul> + <li><strong>Product category:</strong> "electronics", "clothing", "food"</li> + <li><strong>Campaign:</strong> "summer-2025", "black-friday"</li> + <li><strong>Location:</strong> "store-nyc", "warehouse-la"</li> + <li><strong>Status:</strong> "active", "archived", "seasonal"</li> + </ul> + <p>This enables bulk filtering and management in your dashboard later.</p> + + <h3>3. Test Scannability Before Mass Printing</h3> + <p>Before printing 10,000 QR codes:</p> + <ol> + <li>Print 5-10 test codes on the actual material (paper, vinyl, etc.)</li> + <li>Scan from multiple devices (iOS, Android, different scanner apps)</li> + <li>Test various distances: 6 inches, 12 inches, 24 inches</li> + <li>Check different lighting: bright sun, indoor, dim light</li> + <li>Verify destination URLs are correct</li> + </ol> + + <h3>4. Naming Convention Best Practices</h3> + <p>Use smart naming in your <code>name</code> column:</p> + <ul> + <li><strong>Sequential:</strong> PROD-001, PROD-002, PROD-003</li> + <li><strong>Hierarchical:</strong> NYC-STORE-001, NYC-STORE-002, LA-STORE-001</li> + <li><strong>Descriptive:</strong> Include product name: "PROD-001-Laptop-Dell-5420"</li> + </ul> + <p>Downloaded files will have these names, making organization easy.</p> + + <h3>5. Automate with API (Advanced)</h3> + <p>For recurring bulk generation needs, use QR Master's API:</p> + <pre class="bg-gray-100 p-4 rounded-lg my-4"> +// Example: Node.js API call +const response = await fetch('https://api.qrmaster.net/v1/bulk', { + method: 'POST', + headers: { + 'Authorization': 'Bearer YOUR_API_KEY', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + codes: [ + { name: 'Product A', url: 'https://example.com/a' }, + { name: 'Product B', url: 'https://example.com/b' } + ] + }) +}); + </pre> + <p>Perfect for integrating with inventory systems, e-commerce platforms, or automated workflows.</p> + + <h2>Common Mistakes to Avoid</h2> + + <h3>Mistake 1: Using Static QR Codes for Bulk</h3> + <p><strong>Problem:</strong> Generate 5,000 static QR codes, then URL structure changes—all codes are now useless.</p> + <p><strong>Solution:</strong> Always use dynamic QR codes for bulk generation. The small monthly cost ($29) is insignificant compared to reprint costs.</p> + + <h3>Mistake 2: Not Testing Before Mass Printing</h3> + <p><strong>Problem:</strong> Print 10,000 labels, discover QR codes are too small to scan reliably.</p> + <p><strong>Solution:</strong> Print 10 test labels, scan with multiple devices in various conditions before committing to full print run.</p> + + <h3>Mistake 3: Poor File Organization</h3> + <p><strong>Problem:</strong> Download 500 QR codes all named "qr-1.png", "qr-2.png"—impossible to identify which is which.</p> + <p><strong>Solution:</strong> Use descriptive names in your Excel <code>name</code> column: "SKU-001-ProductA", "SKU-002-ProductB".</p> + + <h3>Mistake 4: Forgetting URL Protocols</h3> + <p><strong>Problem:</strong> URLs like <code>example.com</code> (missing https://) cause QR scanners to fail or treat as plain text.</p> + <p><strong>Solution:</strong> Always include full URL: <code>https://example.com</code>. Double-check all URLs before upload.</p> + + <h3>Mistake 5: Exceeding Service Limits</h3> + <p><strong>Problem:</strong> Upload 1,000 codes on a plan that supports only 500.</p> + <p><strong>Solution:</strong> Check your plan limits. Split large batches or upgrade plan before uploading.</p> + + <h2>Conclusion</h2> + + <p>Bulk QR code generation from Excel transforms hours of tedious manual work into minutes of automated efficiency. For any project requiring more than 10-20 QR codes, bulk generation is the only practical approach.</p> + + <p><strong>Key Takeaways:</strong></p> + <ul> + <li>Excel/CSV format: <code>name</code>, <code>url</code>, <code>description</code>, <code>tags</code></li> + <li>Always use <strong>dynamic QR codes</strong> for bulk (editable + trackable)</li> + <li>Test with 5-10 codes before mass printing</li> + <li>QR Master Business ($29/mo) supports up to 500 codes with bulk upload</li> + <li>Time saved: 16-40 hours for 500 codes</li> + </ul> + + <p>Whether you're managing product labels, event tickets, asset tracking, or marketing campaigns, bulk QR generation is an essential productivity tool. Start with a small test batch, optimize your process, then scale to thousands of codes with confidence.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Ready to Generate Bulk QR Codes?</h3> + <p class="text-lg text-gray-700 mb-6">Start with QR Master Business plan: 500 codes, bulk Excel/CSV upload, full analytics. 14-day money-back guarantee.</p> + <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Start Bulk Generation →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/bulk-qr-code-generator">Bulk QR Code Generator</a> - Create hundreds of codes from Excel</li> + <li><a href="/blog/qr-code-tracking-guide-2025">QR Code Tracking Guide</a> - Track every scan</li> + <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a> - Understand the difference</li> + <li><a href="/pricing">Pricing Plans</a> - Compare plans and features</li> + <li><a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">QR Code on Wikipedia</a> - Technical standards (ISO/IEC 18004)</li> + </ul> + </div>`, + }, + + // ============ NEW BLOG POSTS ============ + + 'qr-code-restaurant-menu': { + slug: 'qr-code-restaurant-menu', + title: 'How to Create a QR Code for Restaurant Menu: Complete 2025 Guide', + excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.', + date: 'January 5, 2026', + datePublished: '2026-01-05T09:00:00Z', + dateModified: '2026-01-05T09:00:00Z', + readTime: '12 Min', + category: 'Restaurant', + image: '/blog/restaurant-qr-menu.png', + imageAlt: 'Restaurant table with QR code menu card and smartphone scanning', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'To create a QR code for your restaurant menu, use a dynamic QR code generator like QR Master. Upload your menu PDF or link to your online menu, customize the QR code design, print it on table tents or cards, and track scans to understand customer engagement.', + howTo: { + name: 'How to Create a Restaurant Menu QR Code', + description: 'Complete guide to setting up touchless digital menus with QR codes', + totalTime: 'PT15M', + steps: [ + { + name: 'Prepare Your Digital Menu', + text: 'Create a mobile-friendly menu using PDF, Google Docs, or a dedicated menu platform. Ensure it loads quickly on smartphones.', + }, + { + name: 'Generate a Dynamic QR Code', + text: 'Use QR Master to create a dynamic QR code. This allows you to update your menu URL anytime without reprinting codes.', + url: 'https://www.qrmaster.net/create', + }, + { + name: 'Customize Your QR Code Design', + text: 'Add your restaurant logo, match brand colors, and ensure high contrast for easy scanning.', + }, + { + name: 'Print and Place Strategically', + text: 'Print QR codes on table tents, coasters, or wall-mounted displays. Minimum size: 2x2 inches for table scanning.', + }, + { + name: 'Track and Optimize', + text: 'Monitor scan analytics in your QR Master dashboard to understand peak times and popular menu items.', + url: 'https://www.qrmaster.net/analytics', + }, + ], + }, + content: `<div class="blog-content"> + <h2>Why Restaurants Need QR Code Menus in 2025</h2> + <p>Digital QR code menus have evolved from a pandemic necessity to a restaurant industry standard. In 2025, over 60% of diners prefer scanning a QR code over handling physical menus. For restaurant owners, QR menus offer significant benefits: reduced printing costs, instant menu updates, and valuable customer analytics.</p> + <p>Whether you run a fine dining establishment, casual cafĂ©, or food truck, implementing a <strong>restaurant menu QR code</strong> system can streamline operations and enhance the guest experience.</p> + + <h2>Step 1: Prepare Your Digital Menu</h2> + <h3>Menu Format Options</h3> + <p>Choose the right format for your digital menu:</p> + <ul> + <li><strong>PDF Menu:</strong> Simple and universal. Upload your existing menu design as a PDF for instant access.</li> + <li><strong>Website/Landing Page:</strong> Create a dedicated menu page on your website with images and descriptions.</li> + <li><strong>Menu Platform:</strong> Use services like Square, Toast, or dedicated menu apps for interactive features.</li> + <li><strong>Google Doc:</strong> Free option that allows real-time updates shared via link.</li> + </ul> + + <h3>Mobile Optimization Tips</h3> + <p>Your digital menu must be mobile-friendly since 95% of scans come from smartphones:</p> + <ul> + <li>Use readable font sizes (minimum 16px)</li> + <li>Ensure fast load times (under 3 seconds)</li> + <li>Make buttons and links thumb-friendly</li> + <li>Test on both iOS and Android devices</li> + </ul> + + <h2>Step 2: Create Your QR Code with QR Master</h2> + + <div class="my-8"> + <img src="/blog/restaurant-qr-body.png" alt="Customer scanning QR code menu at restaurant" class="rounded-lg shadow-lg w-full" /> + </div> + + <p>Using a <a href="/dynamic-qr-code-generator">dynamic QR code generator</a> is essential for restaurants. Unlike static codes, dynamic QR codes let you:</p> + <ul> + <li><strong>Update your menu URL anytime</strong> without reprinting QR codes</li> + <li><strong>Track scan analytics</strong> to understand customer behavior</li> + <li><strong>A/B test different landing pages</strong> for seasonal menus</li> + <li><strong>Schedule changes</strong> for lunch vs. dinner menus</li> + </ul> + + <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> + <h3 class="text-xl font-semibold mb-2">Pro Tip: Use Dynamic QR Codes</h3> + <p>Static QR codes encode the URL directly—if your menu URL changes, you need new codes. Dynamic codes redirect through our servers, allowing unlimited URL updates. <a href="/blog/dynamic-vs-static-qr-codes">Learn the difference</a>.</p> + </div> + + <h2>Step 3: Customize Your Restaurant QR Code</h2> + <p>Branding matters. A generic black-and-white QR code looks out of place in a well-designed restaurant. Customize your code to match your brand:</p> + <ul> + <li><strong>Add your logo:</strong> Place your restaurant logo in the center of the QR code</li> + <li><strong>Match brand colors:</strong> Use your brand's color palette for foreground and background</li> + <li><strong>Choose corner styles:</strong> Rounded corners for casual vibes, square for modern/minimal</li> + <li><strong>Maintain contrast:</strong> Ensure minimum 3:1 contrast ratio for reliable scanning</li> + </ul> + + <h2>Step 4: Print and Placement Best Practices</h2> + <h3>Optimal QR Code Sizes for Restaurants</h3> + <table class="w-full border-collapse my-6"> + <thead> + <tr class="bg-gray-100"> + <th class="border p-3 text-left">Placement</th> + <th class="border p-3 text-left">Minimum Size</th> + <th class="border p-3 text-left">Recommended Size</th> + </tr> + </thead> + <tbody> + <tr><td class="border p-3">Table tent</td><td class="border p-3">2" x 2"</td><td class="border p-3">2.5" x 2.5"</td></tr> + <tr><td class="border p-3">Coaster</td><td class="border p-3">1.5" x 1.5"</td><td class="border p-3">2" x 2"</td></tr> + <tr><td class="border p-3">Wall poster</td><td class="border p-3">4" x 4"</td><td class="border p-3">6" x 6"</td></tr> + <tr><td class="border p-3">Window decal</td><td class="border p-3">3" x 3"</td><td class="border p-3">4" x 4"</td></tr> + </tbody> + </table> + + <p>Learn more about <a href="/blog/qr-code-print-size-guide">optimal QR code print sizes</a> for various materials.</p> + + <h3>Strategic Placement Locations</h3> + <ul> + <li><strong>On every table:</strong> Table tents or built-in holders</li> + <li><strong>At the entrance:</strong> Allow guests to browse while waiting</li> + <li><strong>On takeout packaging:</strong> Link to your full menu or loyalty program</li> + <li><strong>At the bar:</strong> Separate drink menu access</li> + </ul> + + <h2>Step 5: Track and Analyze Menu Scans</h2> + <p>With <a href="/qr-code-tracking">QR code tracking</a>, you gain valuable insights:</p> + <ul> + <li><strong>Peak scanning times:</strong> Understand when guests are viewing your menu</li> + <li><strong>Device types:</strong> Optimize for the most common devices</li> + <li><strong>Scan locations:</strong> See which tables or areas have most engagement</li> + <li><strong>Repeat scans:</strong> Identify returning customers</li> + </ul> + + <h2>Common Mistakes to Avoid</h2> + <ul> + <li>❌ Using static QR codes (can't update menu URL)</li> + <li>❌ Too small print size (under 1.5 inches)</li> + <li>❌ Poor lighting near QR code placement</li> + <li>❌ Linking to non-mobile-friendly PDFs</li> + <li>❌ No call-to-action text near the code</li> + </ul> + + <h2>Conclusion</h2> + <p>Creating a QR code for your restaurant menu is straightforward with the right approach. Use dynamic QR codes for flexibility, customize to match your brand, print at appropriate sizes, and track analytics to continuously improve the guest experience.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Create Your Restaurant Menu QR Code</h3> + <p class="text-lg text-gray-700 mb-6">Start free with QR Master—no credit card required. Update your menu anytime and track every scan.</p> + <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create Menu QR Free →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a></li> + <li><a href="/blog/qr-code-print-size-guide">QR Code Print Size Guide</a></li> + <li><a href="/blog/qr-code-analytics">QR Code Analytics Guide</a></li> + <li><a href="/pricing">Pricing Plans</a></li> + </ul> + </div>`, + }, + + 'vcard-qr-code-generator': { + slug: 'vcard-qr-code-generator', + title: 'Free vCard QR Code Generator: Digital Business Cards Made Easy', + excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.', + date: 'January 5, 2026', + datePublished: '2026-01-05T10:00:00Z', + dateModified: '2026-01-05T10:00:00Z', + readTime: '10 Min', + category: 'Business Cards', + image: '/blog/vcard-qr-code.png', + imageAlt: 'Professional business card with vCard QR code being scanned by smartphone', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'A vCard QR code contains your contact information in a standardized format. When scanned, it allows the recipient to save your name, phone, email, company, and social links directly to their phone contacts with one tap.', + howTo: { + name: 'How to Create a vCard QR Code', + description: 'Step-by-step guide to creating digital business card QR codes', + totalTime: 'PT5M', + steps: [ + { + name: 'Enter Your Contact Information', + text: 'Fill in your name, phone number, email, company, job title, and website URL.', + }, + { + name: 'Add Social Media Links', + text: 'Include LinkedIn, Twitter, or other professional networks you want to share.', + }, + { + name: 'Customize the QR Code Design', + text: 'Match your personal or company branding with custom colors and logo.', + }, + { + name: 'Download and Print', + text: 'Export as SVG or high-resolution PNG for business cards, email signatures, or presentations.', + }, + ], + }, + content: `<div class="blog-content"> + <h2>What is a vCard QR Code?</h2> + <p>A vCard (Virtual Contact File) QR code contains your contact information in a standardized format (.vcf). When someone scans it with their smartphone camera, they can instantly save your details to their contacts—no typing required.</p> + <p>This technology has revolutionized professional networking. Instead of handing out paper business cards that often get lost, a <strong>vCard QR code</strong> ensures your contact information is digitally saved and accessible.</p> + + <h2>Why Use a Digital Business Card QR Code?</h2> + <ul> + <li><strong>Instant Saving:</strong> Recipients add your contact with one tap</li> + <li><strong>Always Up-to-Date:</strong> With dynamic vCards, update your info without new cards</li> + <li><strong>Eco-Friendly:</strong> Reduce paper waste from traditional business cards</li> + <li><strong>Track Engagement:</strong> See who scanned and when</li> + <li><strong>Rich Information:</strong> Include social links, profile photos, and more</li> + </ul> + + <div class="my-8"> + <img src="/blog/vcard-qr-body.png" alt="Business professionals exchanging digital business cards" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>Information You Can Include in a vCard</h2> + <p>A comprehensive vCard QR code can contain:</p> + <ul> + <li><strong>Personal Info:</strong> First name, last name, prefix, suffix</li> + <li><strong>Contact Details:</strong> Mobile, work, and home phone numbers</li> + <li><strong>Email Addresses:</strong> Personal and work email</li> + <li><strong>Company Info:</strong> Company name, job title, department</li> + <li><strong>Address:</strong> Street, city, state, country, postal code</li> + <li><strong>Website:</strong> Personal or company URL</li> + <li><strong>Social Media:</strong> LinkedIn, Twitter, Instagram, Facebook</li> + <li><strong>Profile Photo:</strong> Small image encoded in the vCard</li> + <li><strong>Notes:</strong> Brief description or meeting context</li> + </ul> + + <h2>Static vs Dynamic vCard QR Codes</h2> + <table class="w-full border-collapse my-6"> + <thead> + <tr class="bg-gray-100"> + <th class="border p-3 text-left">Feature</th> + <th class="border p-3 text-left">Static vCard</th> + <th class="border p-3 text-left">Dynamic vCard</th> + </tr> + </thead> + <tbody> + <tr><td class="border p-3">Edit after printing</td><td class="border p-3">❌ No</td><td class="border p-3">✅ Yes</td></tr> + <tr><td class="border p-3">Scan tracking</td><td class="border p-3">❌ No</td><td class="border p-3">✅ Yes</td></tr> + <tr><td class="border p-3">QR code size</td><td class="border p-3">Larger (more data)</td><td class="border p-3">Smaller (redirect URL)</td></tr> + <tr><td class="border p-3">Requires account</td><td class="border p-3">No</td><td class="border p-3">Yes (free)</td></tr> + <tr><td class="border p-3">Works offline</td><td class="border p-3">✅ Yes</td><td class="border p-3">Needs internet</td></tr> + </tbody> + </table> + + <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> + <h3 class="text-xl font-semibold mb-2">Recommendation: Use Dynamic vCards</h3> + <p>If you change jobs, phone numbers, or roles, dynamic vCard QR codes let you update without reprinting business cards. Learn more about <a href="/blog/dynamic-vs-static-qr-codes">dynamic vs static QR codes</a>.</p> + </div> + + <h2>How to Create a vCard QR Code</h2> + <h3>Step 1: Choose Your QR Code Type</h3> + <p>Go to the <a href="/create">QR Master generator</a> and select "Contact Card" or vCard type. Choose between static (data embedded) or dynamic (editable, trackable).</p> + + <h3>Step 2: Enter Your Information</h3> + <p>Fill in the contact form with your details. Required fields typically include:</p> + <ul> + <li>Full name</li> + <li>Primary phone number</li> + <li>Email address</li> + </ul> + <p>Optional but recommended: company name, job title, LinkedIn URL, and website.</p> + + <h3>Step 3: Customize Design</h3> + <p>Make your vCard QR code professional:</p> + <ul> + <li>Add your company logo or headshot</li> + <li>Use brand colors</li> + <li>Ensure good contrast for scanning</li> + </ul> + + <h3>Step 4: Download and Deploy</h3> + <p>Export your QR code in the right format:</p> + <ul> + <li><strong>SVG:</strong> Best for print (scalable, sharp at any size)</li> + <li><strong>PNG (300 DPI):</strong> Good for digital and print</li> + </ul> + + <h2>Where to Use Your vCard QR Code</h2> + <ul> + <li><strong>Business Cards:</strong> Replace or supplement traditional cards</li> + <li><strong>Email Signatures:</strong> Let recipients save your contact instantly</li> + <li><strong>LinkedIn Profile:</strong> Add to your banner or featured section</li> + <li><strong>Conference Badges:</strong> Perfect for networking events</li> + <li><strong>Presentations:</strong> Share contact at the end of talks</li> + <li><strong>Resume/CV:</strong> Modern touch for job applications</li> + </ul> + + <h2>Best Practices for Professional vCards</h2> + <ul> + <li>✅ Keep information current and accurate</li> + <li>✅ Use a professional email address (not personal Gmail)</li> + <li>✅ Include your LinkedIn profile</li> + <li>✅ Test scan before printing in bulk</li> + <li>✅ Use dynamic codes if info may change</li> + <li>❌ Don't overload with too many social links</li> + <li>❌ Avoid personal home addresses</li> + </ul> + + <h2>Conclusion</h2> + <p>vCard QR codes are essential tools for modern professionals. They ensure your contact information is always accessible, up-to-date, and easy to save. Whether you're networking at conferences, meeting clients, or job hunting, a digital business card QR code makes a lasting impression.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Create Your Digital Business Card</h3> + <p class="text-lg text-gray-700 mb-6">Generate a free vCard QR code in seconds. Update anytime, track scans, and share professionally.</p> + <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create vCard QR Free →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/create">QR Code Generator</a></li> + <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a></li> + <li><a href="/blog/qr-code-print-size-guide">QR Code Print Size Guide</a></li> + </ul> + </div>`, + }, + + 'qr-code-small-business': { + slug: 'qr-code-small-business', + title: 'Best QR Code Generator for Small Business: 2025 Complete Guide', + excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.', + date: 'January 5, 2026', + datePublished: '2026-01-05T11:00:00Z', + dateModified: '2026-01-05T11:00:00Z', + readTime: '14 Min', + category: 'Business', + image: '/blog/small-business-qr.png', + imageAlt: 'Small business owner using QR codes for customer engagement', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'The best QR code generator for small business offers dynamic codes with tracking, custom branding, affordable pricing, and easy management. QR Master provides free static codes, 3 free dynamic codes, and Pro plans starting at €9/month for growing businesses.', + content: `<div class="blog-content"> + <h2>Why Small Businesses Need QR Codes</h2> + <p>QR codes have become essential tools for small businesses looking to bridge the gap between physical and digital experiences. From contactless payments to customer feedback, <strong>QR codes for small business</strong> offer affordable, versatile solutions that previously required expensive custom apps.</p> + + <div class="my-8"> + <img src="/blog/small-business-body.png" alt="Customer scanning QR code at retail checkout" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>Top 10 QR Code Use Cases for Small Business</h2> + + <h3>1. Digital Menus & Product Catalogs</h3> + <p>Restaurants, cafĂ©s, and retail stores use QR codes to display menus and catalogs. Customers scan to view products, reducing print costs and enabling instant updates.</p> + <p>👉 <a href="/blog/qr-code-restaurant-menu">See our restaurant menu QR guide</a></p> + + <h3>2. Contactless Payments</h3> + <p>Link QR codes to payment platforms like PayPal, Venmo, or Square. Customers scan and pay without cash or card contact.</p> + + <h3>3. Google Reviews & Feedback</h3> + <p>Create QR codes linking directly to your Google Business review page. Place them on receipts, tables, or follow-up emails to boost review volume.</p> + + <h3>4. Business Cards & Networking</h3> + <p>Replace or enhance traditional business cards with <a href="/blog/vcard-qr-code-generator">vCard QR codes</a> that save contact info directly to phones.</p> + + <h3>5. Social Media Follows</h3> + <p>QR codes linking to Instagram, Facebook, or TikTok profiles help convert in-store visitors to online followers.</p> + + <h3>6. Appointment Booking</h3> + <p>Link to Calendly, Square Appointments, or your booking system. Perfect for salons, consultants, and service businesses.</p> + + <h3>7. Wi-Fi Access</h3> + <p>Create Wi-Fi QR codes for your business—customers scan to connect without asking for passwords.</p> + + <h3>8. Loyalty Programs</h3> + <p>QR codes can register loyalty program sign-ups or redeem points, enhancing customer retention.</p> + + <h3>9. Product Information</h3> + <p>Retail and e-commerce businesses add QR codes to packaging linking to tutorials, specifications, or warranty registration.</p> + + <h3>10. Event Tickets & Check-in</h3> + <p>Event businesses use QR codes as digital tickets for easy validation at entry points.</p> + + <h2>What to Look for in a Small Business QR Solution</h2> + <table class="w-full border-collapse my-6"> + <thead> + <tr class="bg-gray-100"> + <th class="border p-3 text-left">Feature</th> + <th class="border p-3 text-left">Why It Matters</th> + </tr> + </thead> + <tbody> + <tr><td class="border p-3">Dynamic QR Codes</td><td class="border p-3">Update URLs without reprinting</td></tr> + <tr><td class="border p-3">Scan Analytics</td><td class="border p-3">Measure campaign performance</td></tr> + <tr><td class="border p-3">Custom Branding</td><td class="border p-3">Match your brand identity</td></tr> + <tr><td class="border p-3">Bulk Creation</td><td class="border p-3">Create many codes from spreadsheets</td></tr> + <tr><td class="border p-3">Affordable Pricing</td><td class="border p-3">Budget-friendly for SMBs</td></tr> + <tr><td class="border p-3">No Expiration</td><td class="border p-3">Codes work forever (with active plan)</td></tr> + </tbody> + </table> + + <h2>QR Master for Small Business</h2> + <p>QR Master is designed with small businesses in mind:</p> + <ul> + <li><strong>Free Forever:</strong> Unlimited static QR codes, 3 free dynamic codes</li> + <li><strong>Pro Plan (€9/mo):</strong> 50 dynamic codes, full analytics, custom branding</li> + <li><strong>Business Plan (€29/mo):</strong> 500 codes, bulk creation, priority support</li> + </ul> + + <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> + <h3 class="text-xl font-semibold mb-2">Free Trial Available</h3> + <p>Start with our free plan—no credit card required. Upgrade when you need more dynamic codes or advanced features.</p> + </div> + + <h2>Getting Started: Quick Setup Guide</h2> + <ol> + <li><strong>Identify Your Goal:</strong> What do you want customers to do after scanning?</li> + <li><strong>Choose Code Type:</strong> Static for permanent content, dynamic for flexibility</li> + <li><strong>Create Your QR Code:</strong> Use <a href="/create">our generator</a> to design and customize</li> + <li><strong>Print at Right Size:</strong> Follow our <a href="/blog/qr-code-print-size-guide">print size guide</a></li> + <li><strong>Track Performance:</strong> Monitor scans in your <a href="/analytics">analytics dashboard</a></li> + </ol> + + <h2>Common Mistakes Small Businesses Make</h2> + <ul> + <li>❌ Using low-quality or blurry printed codes</li> + <li>❌ Linking to non-mobile-friendly pages</li> + <li>❌ Not testing codes before mass printing</li> + <li>❌ Choosing static codes when URLs might change</li> + <li>❌ Missing call-to-action near the QR code</li> + </ul> + + <h2>Conclusion</h2> + <p>QR codes offer small businesses powerful, affordable tools to enhance customer experiences and streamline operations. By choosing the right generator with dynamic capabilities and analytics, you can maximize your ROI and stay competitive in 2025.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Start Your QR Code Strategy Today</h3> + <p class="text-lg text-gray-700 mb-6">Join thousands of small businesses using QR Master for marketing, payments, and customer engagement.</p> + <a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Get Started Free →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/blog/qr-code-restaurant-menu">Restaurant Menu QR Guide</a></li> + <li><a href="/blog/vcard-qr-code-generator">vCard Business Card Generator</a></li> + <li><a href="/blog/qr-code-analytics">QR Code Analytics Guide</a></li> + <li><a href="/pricing">View Pricing Plans</a></li> + </ul> + </div>`, + }, + + 'qr-code-print-size-guide': { + slug: 'qr-code-print-size-guide', + title: 'QR Code Print Size Guide: Minimum Sizes for Every Use Case', + excerpt: 'Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.', + date: 'January 5, 2026', + datePublished: '2026-01-05T12:00:00Z', + dateModified: '2026-01-05T12:00:00Z', + readTime: '8 Min', + category: 'Printing', + image: '/blog/qr-print-sizes.png', + imageAlt: 'Various print materials showing different QR code sizes', + author: 'QR Master Team', + authorUrl: 'https://www.qrmaster.net/about', + answer: 'The minimum QR code size depends on scanning distance. For close scanning (business cards), minimum is 0.8" x 0.8" (2cm). For 6-foot distance (posters), minimum is 6" x 6" (15cm). Rule of thumb: QR size = scanning distance Ă· 10.', + content: `<div class="blog-content"> + <h2>Why QR Code Size Matters</h2> + <p>A QR code that's too small won't scan reliably, frustrating customers and wasting your printing investment. Understanding the relationship between <strong>QR code print size</strong>, scanning distance, and data density is essential for successful QR campaigns.</p> + + <h2>The Scanning Distance Formula</h2> + <p>The golden rule for QR code sizing:</p> + <div class="bg-gray-100 p-6 rounded-lg my-6 text-center"> + <p class="text-2xl font-bold text-gray-900">QR Code Width = Scanning Distance Ă· 10</p> + <p class="text-gray-600 mt-2">Example: 3 feet scanning distance = 3.6 inch QR code</p> + </div> + + <div class="my-8"> + <img src="/blog/qr-sizes-body.png" alt="Various QR code print sizes comparison" class="rounded-lg shadow-lg w-full" /> + </div> + + <h2>QR Code Sizes by Application</h2> + <table class="w-full border-collapse my-6"> + <thead> + <tr class="bg-gray-100"> + <th class="border p-3 text-left">Application</th> + <th class="border p-3 text-left">Scanning Distance</th> + <th class="border p-3 text-left">Minimum Size</th> + <th class="border p-3 text-left">Recommended</th> + </tr> + </thead> + <tbody> + <tr><td class="border p-3">Business Card</td><td class="border p-3">4-8 inches</td><td class="border p-3">0.8" (2cm)</td><td class="border p-3">1" (2.5cm)</td></tr> + <tr><td class="border p-3">Product Label</td><td class="border p-3">6-12 inches</td><td class="border p-3">0.6" (1.5cm)</td><td class="border p-3">1" (2.5cm)</td></tr> + <tr><td class="border p-3">Flyer/Brochure</td><td class="border p-3">1-2 feet</td><td class="border p-3">1.2" (3cm)</td><td class="border p-3">1.5" (4cm)</td></tr> + <tr><td class="border p-3">Table Tent</td><td class="border p-3">1-3 feet</td><td class="border p-3">2" (5cm)</td><td class="border p-3">2.5" (6cm)</td></tr> + <tr><td class="border p-3">Poster (indoor)</td><td class="border p-3">3-6 feet</td><td class="border p-3">4" (10cm)</td><td class="border p-3">6" (15cm)</td></tr> + <tr><td class="border p-3">Banner (outdoor)</td><td class="border p-3">6-15 feet</td><td class="border p-3">8" (20cm)</td><td class="border p-3">12" (30cm)</td></tr> + <tr><td class="border p-3">Billboard</td><td class="border p-3">15+ feet</td><td class="border p-3">18" (45cm)</td><td class="border p-3">24" (60cm)</td></tr> + </tbody> + </table> + + <h2>Factors Affecting Scanability</h2> + + <h3>1. Data Density</h3> + <p>More data = more modules = harder to scan at small sizes. Dynamic QR codes contain short redirect URLs, making them easier to scan at smaller sizes than static codes with long URLs.</p> + + <h3>2. Error Correction Level</h3> + <p>QR codes have four error correction levels:</p> + <ul> + <li><strong>L (7%):</strong> Smallest codes, least damage tolerance</li> + <li><strong>M (15%):</strong> Standard, good balance</li> + <li><strong>Q (25%):</strong> Higher tolerance, larger codes</li> + <li><strong>H (30%):</strong> Maximum tolerance, largest codes (needed for logos)</li> + </ul> + + <h3>3. Print Quality</h3> + <p>Low DPI printing blurs the code's modules. Recommended resolutions:</p> + <ul> + <li><strong>Minimum:</strong> 150 DPI</li> + <li><strong>Recommended:</strong> 300 DPI</li> + <li><strong>Best (small codes):</strong> 600 DPI</li> + </ul> + + <h3>4. Contrast</h3> + <p>Maintain minimum 3:1 contrast ratio between foreground and background. Avoid:</p> + <ul> + <li>Light gray on white</li> + <li>Similar color tones</li> + <li>Glossy surfaces with glare</li> + </ul> + + <h2>Quiet Zone Requirements</h2> + <p>The "quiet zone" is the blank margin around your QR code. Standard requirement:</p> + <div class="bg-gray-100 p-6 rounded-lg my-6 text-center"> + <p class="text-xl font-bold text-gray-900">Quiet Zone = 4 × Module Size</p> + <p class="text-gray-600 mt-2">Always leave white space around your QR code</p> + </div> + + <h2>File Formats for Printing</h2> + <table class="w-full border-collapse my-6"> + <thead> + <tr class="bg-gray-100"> + <th class="border p-3 text-left">Format</th> + <th class="border p-3 text-left">Best For</th> + <th class="border p-3 text-left">Scalability</th> + </tr> + </thead> + <tbody> + <tr><td class="border p-3">SVG</td><td class="border p-3">All print applications</td><td class="border p-3">∞ (vector)</td></tr> + <tr><td class="border p-3">PDF</td><td class="border p-3">Professional printing</td><td class="border p-3">∞ (vector)</td></tr> + <tr><td class="border p-3">PNG (300 DPI)</td><td class="border p-3">Digital and standard print</td><td class="border p-3">Limited</td></tr> + <tr><td class="border p-3">EPS</td><td class="border p-3">Professional design software</td><td class="border p-3">∞ (vector)</td></tr> + </tbody> + </table> + + <div class="bg-blue-50 border-l-4 border-blue-500 p-6 my-8 rounded-r-lg"> + <h3 class="text-xl font-semibold mb-2">Pro Tip: Always Use SVG</h3> + <p>Download your QR codes as SVG for infinite scalability. Scale up for billboards or down for business cards without losing quality.</p> + </div> + + <h2>Testing Before Printing</h2> + <p>Always test your QR codes before bulk printing:</p> + <ol> + <li>Print a test sample at actual size</li> + <li>Scan with multiple devices (iOS, Android)</li> + <li>Test from the intended scanning distance</li> + <li>Check under actual lighting conditions</li> + <li>Verify the destination URL works correctly</li> + </ol> + + <h2>Conclusion</h2> + <p>Proper QR code sizing ensures reliable scanning and protects your printing investment. Remember the distance Ă· 10 formula, always leave adequate quiet zones, and use vector formats for scalability. When in doubt, go slightly larger—a readable code is always better than a sleek but unscannable one.</p> + + <div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200"> + <h3 class="text-2xl font-bold text-gray-900 mb-4">Create Print-Ready QR Codes</h3> + <p class="text-lg text-gray-700 mb-6">Download high-resolution SVG and PNG files ready for any print application.</p> + <a href="/create" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create QR Code →</a> + </div> + + <h2>Related Resources</h2> + <ul> + <li><a href="/blog/qr-code-restaurant-menu">Restaurant Menu QR Guide</a></li> + <li><a href="/blog/bulk-qr-codes-excel">Bulk QR Code Generation</a></li> + <li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a></li> + </ul> + </div>`, + }, +}; + +function truncateAtWord(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + const truncated = text.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(' '); + return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; +} + +export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { + const post = blogPosts[params.slug]; + + if (!post) { + return { + title: 'Post Not Found', + }; + } + + const title = truncateAtWord(`${post.title} - QR Analytics Tips`, 60); + const description = truncateAtWord(post.excerpt, 160); + + return { + title, + description, + alternates: { + canonical: `https://www.qrmaster.net/blog/${params.slug}`, + languages: { + 'x-default': `https://www.qrmaster.net/blog/${params.slug}`, + en: `https://www.qrmaster.net/blog/${params.slug}`, + }, + }, + openGraph: { + title, + description, + url: `https://www.qrmaster.net/blog/${params.slug}`, + type: 'article', + publishedTime: post.datePublished, + modifiedTime: post.dateModified, + authors: [post.author], + images: [ + { + url: post.image, + width: 1200, + height: 630, + alt: post.imageAlt, + }, + ], + }, + twitter: { + title, + description, + card: 'summary_large_image', + images: [post.image], + }, + }; +} + +export default function BlogPostPage({ params }: { params: { slug: string } }) { + const post = blogPosts[params.slug]; + + if (!post) { + notFound(); + } + + const breadcrumbItems: BreadcrumbItem[] = [ + { name: 'Home', url: '/' }, + { name: 'Blog', url: '/blog' }, + { name: post.title, url: `/blog/${post.slug}` }, + ]; + + const schemas: any[] = [ + blogPostingSchema({ + title: post.title, + description: post.excerpt, + slug: post.slug, + author: post.author, + authorUrl: post.authorUrl, + datePublished: post.datePublished, + dateModified: post.dateModified, + image: post.image, + }), + breadcrumbSchema(breadcrumbItems), + ]; + + if (post.howTo) { + schemas.push(howToSchema(post.howTo)); + } + + return ( + <> + <SeoJsonLd data={schemas} /> + <div className="py-20 bg-gradient-to-b from-gray-50 to-white"> + <div className="container mx-auto px-4"> + <div className="max-w-4xl mx-auto"> + <Breadcrumbs items={breadcrumbItems} /> + + <article className="bg-white rounded-2xl shadow-sm p-8 md:p-12"> + <header className="mb-10"> + <div className="flex items-center flex-wrap gap-3 mb-6"> + <Badge variant="info">{post.category}</Badge> + <span className="text-gray-500 flex items-center"> + <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> + {post.readTime} read + </span> + <span className="text-gray-500">By {post.author}</span> + <span className="text-gray-500">{post.date}</span> + </div> + + <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> + {post.title} + </h1> + + {post.answer && ( + <div className="bg-blue-50 border-l-4 border-blue-500 p-6 mb-8 rounded-r-lg"> + <h2 className="text-xl font-semibold mb-2 text-gray-900">Quick Answer</h2> + <p className="text-lg text-gray-800 leading-relaxed">{post.answer}</p> + </div> + )} + + <div className="relative w-full h-96 rounded-2xl overflow-hidden shadow-lg mb-8"> + <Image + src={post.image} + alt={post.imageAlt} + fill + className="object-cover" + priority + /> + </div> + </header> + + <div + className="prose prose-lg max-w-none + prose-headings:font-bold prose-headings:text-gray-900 + prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 + prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4 + prose-p:text-gray-700 prose-p:leading-relaxed prose-p:mb-6 prose-p:text-lg + prose-ul:my-6 prose-ul:space-y-2 + prose-li:text-gray-700 prose-li:leading-relaxed + prose-strong:text-gray-900 prose-strong:font-semibold" + dangerouslySetInnerHTML={{ __html: post.content }} + /> + + {post.howTo && ( + <div className="mt-12 bg-gradient-to-br from-blue-50 to-indigo-50 p-8 rounded-2xl border border-blue-200"> + <h2 className="text-3xl font-bold text-gray-900 mb-6">{post.howTo.name}</h2> + <p className="text-lg text-gray-700 mb-6 leading-relaxed">{post.howTo.description}</p> + <ol className="space-y-6"> + {post.howTo.steps.map((step: any, index: number) => ( + <li key={index} className="flex items-start"> + <span className="flex-shrink-0 w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold text-lg mr-4"> + {index + 1} + </span> + <div className="flex-1"> + <h3 className="font-semibold text-xl mb-2 text-gray-900">{step.name}</h3> + <p className="text-gray-700 leading-relaxed">{step.text}</p> + </div> + </li> + ))} + </ol> + </div> + )} + + <div className="mt-16 p-10 bg-gradient-to-br from-primary-50 to-primary-100 rounded-2xl text-center border border-primary-200"> + <h2 className="text-3xl font-bold text-gray-900 mb-4"> + Ready to Track Your QR Campaigns? + </h2> + <p className="text-lg text-gray-700 mb-8 max-w-2xl mx-auto leading-relaxed"> + Start creating professional dynamic QR codes with advanced scan analytics, campaign tracking, and real-time insights. + </p> + <Link href="/signup"> + <Button size="lg">Create QR Code Free</Button> + </Link> + </div> + + {/* Related Articles Section */} + <div className="mt-16"> + <h2 className="text-2xl font-bold text-gray-900 mb-8">Related Articles</h2> + <div className="overflow-x-auto pb-4 -mx-4 px-4"> + <div className="flex gap-6" style={{ minWidth: 'max-content' }}> + {Object.values(blogPosts) + .filter((p) => p.slug !== post.slug) + .map((relatedPost) => ( + <Link + key={relatedPost.slug} + href={`/blog/${relatedPost.slug}`} + className="group block bg-gray-50 rounded-xl p-6 hover:bg-gray-100 transition-colors flex-shrink-0" + style={{ width: '320px' }} + > + <Badge variant="default" className="mb-3">{relatedPost.category}</Badge> + <h3 className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors mb-2 line-clamp-2"> + {relatedPost.title} + </h3> + <p className="text-sm text-gray-600 line-clamp-2">{relatedPost.excerpt}</p> + <span className="text-sm text-primary-600 mt-3 inline-block">Read more →</span> + </Link> + ))} + </div> + </div> + </div> + </article> + </div> + </div> + </div> + </> + ); +} diff --git a/src/app/(marketing)/blog/page.tsx b/src/app/(marketing)/blog/page.tsx index 0850ec7..983e260 100644 --- a/src/app/(marketing)/blog/page.tsx +++ b/src/app/(marketing)/blog/page.tsx @@ -1,182 +1,182 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import Link from 'next/link'; -import Image from 'next/image'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import { websiteSchema, breadcrumbSchema } from '@/lib/schema'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; -import { Badge } from '@/components/ui/Badge'; -import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; - -function truncateAtWord(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - const truncated = text.slice(0, maxLength); - const lastSpace = truncated.lastIndexOf(' '); - return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; -} - -export async function generateMetadata(): Promise<Metadata> { - const title = truncateAtWord('QR Insights: Latest QR Strategies', 60); - const description = truncateAtWord( - 'Expert guides on QR analytics, dynamic codes & smart marketing uses.', - 160 - ); - - return { - title, - description, - alternates: { - canonical: 'https://www.qrmaster.net/blog', - languages: { - 'x-default': 'https://www.qrmaster.net/blog', - en: 'https://www.qrmaster.net/blog', - }, - }, - openGraph: { - title, - description, - url: 'https://www.qrmaster.net/blog', - type: 'website', - }, - twitter: { - title, - description, - }, - }; -} - -const blogPosts = [ - // NEW POSTS (January 2026) - { - slug: 'qr-code-restaurant-menu', - title: 'How to Create a QR Code for Restaurant Menu', - excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.', - date: 'January 5, 2026', - readTime: '12 Min', - category: 'Restaurant', - image: '/blog/restaurant-qr-menu.png', - }, - { - slug: 'vcard-qr-code-generator', - title: 'Free vCard QR Code Generator: Digital Business Cards', - excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.', - date: 'January 5, 2026', - readTime: '10 Min', - category: 'Business Cards', - image: '/blog/vcard-qr-code.png', - }, - { - slug: 'qr-code-small-business', - title: 'Best QR Code Generator for Small Business: 2025 Guide', - excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.', - date: 'January 5, 2026', - readTime: '14 Min', - category: 'Business', - image: '/blog/small-business-qr.png', - }, - { - slug: 'qr-code-print-size-guide', - title: 'QR Code Print Size Guide: Minimum Sizes for Every Use Case', - excerpt: 'Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.', - date: 'January 5, 2026', - readTime: '8 Min', - category: 'Printing', - image: '/blog/qr-print-sizes.png', - }, - // EXISTING POSTS - { - slug: 'qr-code-tracking-guide-2025', - title: 'QR Code Tracking: Complete Guide 2025', - excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools, setup Google Analytics, and measure ROI.', - date: 'October 18, 2025', - readTime: '12 Min', - category: 'Tracking & Analytics', - image: '/blog/1-hero.png', - }, - { - slug: 'dynamic-vs-static-qr-codes', - title: 'Dynamic vs Static QR Codes: Which Should You Use?', - excerpt: 'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money.', - date: 'October 17, 2025', - readTime: '10 Min', - category: 'QR Code Basics', - image: '/blog/2-hero.png', - }, - { - slug: 'bulk-qr-code-generator-excel', - title: 'How to Generate Bulk QR Codes from Excel', - excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.', - date: 'October 16, 2025', - readTime: '13 Min', - category: 'Bulk Generation', - image: '/blog/3-hero.png', - }, - { - slug: 'qr-code-analytics', - title: 'QR Code Analytics: Track, Measure & Optimize Campaigns', - excerpt: 'Learn how to leverage scan analytics, campaign tracking, and dashboard insights to maximize QR code ROI.', - date: 'October 16, 2025', - readTime: '15 Min', - category: 'Analytics', - image: '/blog/4-hero.png', - }, -]; - -export default function BlogPage() { - const breadcrumbItems: BreadcrumbItem[] = [ - { name: 'Home', url: '/' }, - { name: 'Blog', url: '/blog' }, - ]; - - return ( - <> - <SeoJsonLd data={[websiteSchema(), breadcrumbSchema(breadcrumbItems)]} /> - <div className="py-20 bg-gradient-to-b from-gray-50 to-white"> - <div className="container mx-auto px-4"> - <Breadcrumbs items={breadcrumbItems} /> - <div className="text-center mb-16"> - <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> - QR Code Insights - </h1> - <p className="text-xl text-gray-600 max-w-2xl mx-auto"> - Expert guides on dynamic QR codes, campaign tracking, UTM analytics, and smart marketing use cases. - Discover how-to tutorials and best practices for QR code analytics. - </p> - </div> - - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto"> - {blogPosts.map((post) => ( - <Link key={post.slug} href={`/blog/${post.slug}`}> - <Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300"> - <div className="relative h-56 overflow-hidden"> - <Image - src={post.image} - alt={`${post.title} - QR code guide showing ${post.category.toLowerCase()} strategies`} - width={800} - height={600} - className="w-full h-full object-cover transition-transform duration-500 hover:scale-110" - /> - </div> - <CardHeader className="pb-3"> - <div className="flex items-center justify-between mb-3"> - <Badge variant="info">{post.category}</Badge> - <span className="text-sm text-gray-500 font-medium">{post.readTime} read</span> - </div> - <CardTitle className="text-xl leading-tight mb-3">{post.title}</CardTitle> - </CardHeader> - <CardContent className="pt-0"> - <p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p> - <div className="flex items-center justify-between pt-4 border-t border-gray-100"> - <p className="text-sm text-gray-500">{post.date}</p> - <span className="text-primary-600 text-sm font-medium">Read more →</span> - </div> - </CardContent> - </Card> - </Link> - ))} - </div> - </div> - </div> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import { websiteSchema, breadcrumbSchema } from '@/lib/schema'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Badge } from '@/components/ui/Badge'; +import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; + +function truncateAtWord(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + const truncated = text.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(' '); + return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; +} + +export async function generateMetadata(): Promise<Metadata> { + const title = truncateAtWord('QR Insights: Latest QR Strategies', 60); + const description = truncateAtWord( + 'Expert guides on QR analytics, dynamic codes & smart marketing uses.', + 160 + ); + + return { + title, + description, + alternates: { + canonical: 'https://www.qrmaster.net/blog', + languages: { + 'x-default': 'https://www.qrmaster.net/blog', + en: 'https://www.qrmaster.net/blog', + }, + }, + openGraph: { + title, + description, + url: 'https://www.qrmaster.net/blog', + type: 'website', + }, + twitter: { + title, + description, + }, + }; +} + +const blogPosts = [ + // NEW POSTS (January 2026) + { + slug: 'qr-code-restaurant-menu', + title: 'How to Create a QR Code for Restaurant Menu', + excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Learn best practices for touchless menus, placement tips, and tracking.', + date: 'January 5, 2026', + readTime: '12 Min', + category: 'Restaurant', + image: '/blog/restaurant-qr-menu.png', + }, + { + slug: 'vcard-qr-code-generator', + title: 'Free vCard QR Code Generator: Digital Business Cards', + excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly with a scan—includes templates and best practices.', + date: 'January 5, 2026', + readTime: '10 Min', + category: 'Business Cards', + image: '/blog/vcard-qr-code.png', + }, + { + slug: 'qr-code-small-business', + title: 'Best QR Code Generator for Small Business: 2025 Guide', + excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases for marketing, payments, and operations.', + date: 'January 5, 2026', + readTime: '14 Min', + category: 'Business', + image: '/blog/small-business-qr.png', + }, + { + slug: 'qr-code-print-size-guide', + title: 'QR Code Print Size Guide: Minimum Sizes for Every Use Case', + excerpt: 'Complete guide to QR code print sizes. Learn minimum dimensions for business cards, posters, banners, and more to ensure reliable scanning.', + date: 'January 5, 2026', + readTime: '8 Min', + category: 'Printing', + image: '/blog/qr-print-sizes.png', + }, + // EXISTING POSTS + { + slug: 'qr-code-tracking-guide-2025', + title: 'QR Code Tracking: Complete Guide 2025', + excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools, setup Google Analytics, and measure ROI.', + date: 'October 18, 2025', + readTime: '12 Min', + category: 'Tracking & Analytics', + image: '/blog/1-hero.png', + }, + { + slug: 'dynamic-vs-static-qr-codes', + title: 'Dynamic vs Static QR Codes: Which Should You Use?', + excerpt: 'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money.', + date: 'October 17, 2025', + readTime: '10 Min', + category: 'QR Code Basics', + image: '/blog/2-hero.png', + }, + { + slug: 'bulk-qr-code-generator-excel', + title: 'How to Generate Bulk QR Codes from Excel', + excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide with templates, best practices, and free tools.', + date: 'October 16, 2025', + readTime: '13 Min', + category: 'Bulk Generation', + image: '/blog/3-hero.png', + }, + { + slug: 'qr-code-analytics', + title: 'QR Code Analytics: Track, Measure & Optimize Campaigns', + excerpt: 'Learn how to leverage scan analytics, campaign tracking, and dashboard insights to maximize QR code ROI.', + date: 'October 16, 2025', + readTime: '15 Min', + category: 'Analytics', + image: '/blog/4-hero.png', + }, +]; + +export default function BlogPage() { + const breadcrumbItems: BreadcrumbItem[] = [ + { name: 'Home', url: '/' }, + { name: 'Blog', url: '/blog' }, + ]; + + return ( + <> + <SeoJsonLd data={[websiteSchema(), breadcrumbSchema(breadcrumbItems)]} /> + <div className="py-20 bg-gradient-to-b from-gray-50 to-white"> + <div className="container mx-auto px-4"> + <Breadcrumbs items={breadcrumbItems} /> + <div className="text-center mb-16"> + <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> + QR Code Insights + </h1> + <p className="text-xl text-gray-600 max-w-2xl mx-auto"> + Expert guides on dynamic QR codes, campaign tracking, UTM analytics, and smart marketing use cases. + Discover how-to tutorials and best practices for QR code analytics. + </p> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto"> + {blogPosts.map((post) => ( + <Link key={post.slug} href={`/blog/${post.slug}`}> + <Card hover className="h-full overflow-hidden shadow-md hover:shadow-xl transition-all duration-300"> + <div className="relative h-56 overflow-hidden"> + <Image + src={post.image} + alt={`${post.title} - QR code guide showing ${post.category.toLowerCase()} strategies`} + width={800} + height={600} + className="w-full h-full object-cover transition-transform duration-500 hover:scale-110" + /> + </div> + <CardHeader className="pb-3"> + <div className="flex items-center justify-between mb-3"> + <Badge variant="info">{post.category}</Badge> + <span className="text-sm text-gray-500 font-medium">{post.readTime} read</span> + </div> + <CardTitle className="text-xl leading-tight mb-3">{post.title}</CardTitle> + </CardHeader> + <CardContent className="pt-0"> + <p className="text-gray-600 mb-4 leading-relaxed">{post.excerpt}</p> + <div className="flex items-center justify-between pt-4 border-t border-gray-100"> + <p className="text-sm text-gray-500">{post.date}</p> + <span className="text-primary-600 text-sm font-medium">Read more →</span> + </div> + </CardContent> + </Card> + </Link> + ))} + </div> + </div> + </div> + </> + ); +} diff --git a/src/app/(marketing)/bulk-qr-code-generator/page.tsx b/src/app/(marketing)/bulk-qr-code-generator/page.tsx index c5c43d4..3c1d566 100644 --- a/src/app/(marketing)/bulk-qr-code-generator/page.tsx +++ b/src/app/(marketing)/bulk-qr-code-generator/page.tsx @@ -1,661 +1,661 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import Link from 'next/link'; -import { Button } from '@/components/ui/Button'; -import { Card } from '@/components/ui/Card'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; -import { breadcrumbSchema } from '@/lib/schema'; - -export const metadata: Metadata = { - title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel | QR Master', - description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.', - keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free', - alternates: { - canonical: 'https://www.qrmaster.net/bulk-qr-code-generator', - languages: { - 'x-default': 'https://www.qrmaster.net/bulk-qr-code-generator', - en: 'https://www.qrmaster.net/bulk-qr-code-generator', - }, - }, - openGraph: { - title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel', - description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.', - url: 'https://www.qrmaster.net/bulk-qr-code-generator', - type: 'website', - }, - twitter: { - title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel', - description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.', - }, -}; - -export default function BulkQRCodeGeneratorPage() { - const qrCodeTypes = [ - { - type: 'URL', - icon: '🌐', - title: 'Website Links', - description: 'Generate QR codes for websites, landing pages, and online content', - format: 'https://example.com/product', - example: 'Product Page,URL,https://example.com/product', - }, - { - type: 'VCARD', - icon: 'đŸ‘€', - title: 'Contact Cards', - description: 'Create vCard QR codes with contact information', - format: 'FirstName,LastName,Email,Phone,Organization,Title', - example: 'John Doe,VCARD,John,Doe,john@example.com,+1234567890,Company Inc,CEO', - }, - { - type: 'GEO', - icon: '📍', - title: 'Locations', - description: 'Generate location QR codes with GPS coordinates', - format: 'latitude,longitude,label', - example: 'Office Location,GEO,37.7749,-122.4194,Main Office', - }, - { - type: 'PHONE', - icon: '📞', - title: 'Phone Numbers', - description: 'Create QR codes that dial phone numbers', - format: '+1234567890', - example: 'Support Hotline,PHONE,+1234567890', - }, - { - type: 'TEXT', - icon: '📝', - title: 'Plain Text', - description: 'Generate QR codes with any text content', - format: 'Your text content here', - example: 'Serial Number,TEXT,SN-12345-ABCDE', - }, - ]; - - const bulkFeatures = [ - { - icon: '📊', - title: 'Excel & CSV Import', - description: 'Upload Excel or CSV files to generate hundreds of QR codes in seconds. Simple column mapping.', - }, - { - icon: '⚡', - title: 'Fast Processing', - description: 'Generate up to 1000 QR codes in under a minute. Optimized for speed and reliability.', - }, - { - icon: '🎹', - title: 'Unified Branding', - description: 'Apply your logo, colors, and design to all QR codes at once. Consistent brand identity.', - }, - { - icon: '📩', - title: 'Batch Download', - description: 'Download all QR codes as a ZIP file with custom filenames. Organized and ready to use.', - }, - { - icon: '📈', - title: 'Individual Tracking', - description: 'Track each QR code separately. See which products or locations perform best.', - }, - { - icon: '🔄', - title: 'Update in Bulk', - description: 'Edit multiple QR codes at once. Save time when updating campaigns or product info.', - }, - ]; - - const useCases = [ - { - title: 'Product Labels', - icon: 'đŸ·ïž', - description: 'Generate unique QR codes for each product SKU. Link to manuals, warranty info, or product pages.', - stats: ['1000+ products', 'Individual tracking', 'Custom naming'], - }, - { - title: 'Event Tickets', - icon: 'đŸŽŸïž', - description: 'Create unique QR codes for every attendee. Enable fast check-ins and track attendance.', - stats: ['Unique per ticket', 'Real-time validation', 'Analytics dashboard'], - }, - { - title: 'Asset Management', - icon: 'đŸ’Œ', - description: 'Tag equipment, furniture, and assets with unique QR codes. Track location and maintenance.', - stats: ['Equipment tracking', 'Maintenance logs', 'Location history'], - }, - { - title: 'Marketing Campaigns', - icon: '📱', - description: 'Generate codes for different locations or channels. Track which campaigns perform best.', - stats: ['Location-specific', 'Campaign tracking', 'ROI measurement'], - }, - ]; - - const howItWorks = [ - { - step: 1, - title: 'Prepare Your File', - description: 'Create an Excel or CSV file with your URLs, names, and any custom data.', - example: 'Product Name | URL | SKU', - }, - { - step: 2, - title: 'Upload & Map', - description: 'Upload your file and map columns to QR code fields. Preview before generating.', - example: 'Map columns: Name → Title, URL → Destination', - }, - { - step: 3, - title: 'Customize Design', - description: 'Apply logo, colors, and branding to all QR codes at once. Consistent look.', - example: 'Add logo, set colors, choose frame', - }, - { - step: 4, - title: 'Generate & Download', - description: 'Click generate and download all QR codes as PNG files in a ZIP archive.', - example: 'product-001.png, product-002.png, ...', - }, - ]; - - const fileFormat = [ - { column: 'name', description: 'QR code title/label', required: true }, - { column: 'url', description: 'Destination URL', required: true }, - { column: 'description', description: 'Optional description', required: false }, - { column: 'tags', description: 'Comma-separated tags', required: false }, - ]; - - const softwareSchema = { - '@context': 'https://schema.org', - '@type': 'SoftwareApplication', - '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#software', - name: 'QR Master - Bulk QR Code Generator', - applicationCategory: 'BusinessApplication', - operatingSystem: 'Web Browser', - offers: { - '@type': 'Offer', - price: '0', - priceCurrency: 'USD', - availability: 'https://schema.org/InStock', - }, - aggregateRating: { - '@type': 'AggregateRating', - ratingValue: '4.8', - ratingCount: '980', - }, - description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, inventory management with custom branding.', - featureList: [ - 'Excel and CSV file import', - 'Generate up to 1000 QR codes at once', - 'Unified branding and design', - 'Batch download as ZIP', - 'Individual tracking per code', - 'Bulk update capabilities', - 'Custom filenames', - 'High-resolution exports', - ], - }; - - const howToSchema = { - '@context': 'https://schema.org', - '@type': 'HowTo', - '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#howto', - name: 'How to Generate Bulk QR Codes from Excel', - description: 'Learn how to create hundreds of QR codes from an Excel or CSV file', - totalTime: 'PT10M', - step: [ - { - '@type': 'HowToStep', - position: 1, - name: 'Prepare Excel File', - text: 'Create an Excel or CSV file with columns for name, URL, and any custom data you need', - }, - { - '@type': 'HowToStep', - position: 2, - name: 'Upload File', - text: 'Log into QR Master and upload your file to the bulk generator', - url: 'https://www.qrmaster.net/bulk-creation', - }, - { - '@type': 'HowToStep', - position: 3, - name: 'Map Columns', - text: 'Map your file columns to QR code fields (name, URL, description, etc.)', - }, - { - '@type': 'HowToStep', - position: 4, - name: 'Customize Design', - text: 'Apply your logo, brand colors, and design settings to all QR codes at once', - }, - { - '@type': 'HowToStep', - position: 5, - name: 'Generate and Download', - text: 'Click generate and download all QR codes as a ZIP file with custom filenames', - }, - ], - }; - - const faqSchema = { - '@context': 'https://schema.org', - '@type': 'FAQPage', - '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#faq', - mainEntity: [ - { - '@type': 'Question', - name: 'How many QR codes can I generate at once?', - acceptedAnswer: { - '@type': 'Answer', - text: 'With QR Master, you can generate up to 1000 QR codes at once from a CSV or Excel file. For larger volumes, contact our enterprise team.', - }, - }, - { - '@type': 'Question', - name: 'What file formats are supported?', - acceptedAnswer: { - '@type': 'Answer', - text: 'QR Master supports CSV (.csv), Excel (.xlsx, .xls), and other spreadsheet formats. Simply ensure your file has columns for name and destination URL.', - }, - }, - { - '@type': 'Question', - name: 'Can I apply my branding to all QR codes?', - acceptedAnswer: { - '@type': 'Answer', - text: 'Yes, you can apply your logo, brand colors, and custom design to all QR codes in your bulk generation. All codes will have consistent branding.', - }, - }, - { - '@type': 'Question', - name: 'Are bulk generated QR codes trackable?', - acceptedAnswer: { - '@type': 'Answer', - text: 'Yes, each QR code generated in bulk is individually trackable. You can see scans, locations, and analytics for every single code in your dashboard.', - }, - }, - ], - }; - - const breadcrumbItems: BreadcrumbItem[] = [ - { name: 'Home', url: '/' }, - { name: 'Bulk QR Code Generator', url: '/bulk-qr-code-generator' }, - ]; - - return ( - <> - <SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} /> - <div className="min-h-screen bg-white"> - {/* Hero Section */} - <section className="relative overflow-hidden bg-gradient-to-br from-green-50 via-white to-blue-50 py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <Breadcrumbs items={breadcrumbItems} /> - <div className="grid lg:grid-cols-2 gap-12 items-center"> - <div className="space-y-8"> - <div className="inline-flex items-center space-x-2 bg-green-100 text-green-800 px-4 py-2 rounded-full text-sm font-semibold"> - <span>⚡</span> - <span>Generate 1000s in Minutes</span> - </div> - - <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> - Bulk QR Code Generator - </h1> - - <p className="text-xl text-gray-600 leading-relaxed"> - Create hundreds or thousands of QR codes from Excel or CSV files. Generate URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory, and marketing campaigns. - </p> - - <div className="space-y-3"> - {[ - 'Upload Excel or CSV files', - 'Generate URLs, vCards, locations & more', - 'Custom branding on all codes', - 'Download as organized ZIP', - ].map((feature, index) => ( - <div key={index} className="flex items-center space-x-3"> - <div className="flex-shrink-0 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"> - <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - </div> - <span className="text-gray-700">{feature}</span> - </div> - ))} - </div> - - <div className="flex flex-col sm:flex-row gap-4"> - <Link href="/signup"> - <Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> - Start Bulk Generation - </Button> - </Link> - <Link href="/create"> - <Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> - Try Single QR First - </Button> - </Link> - </div> - </div> - - {/* Visual Example */} - <div className="relative"> - <Card className="p-6 shadow-2xl"> - <h3 className="font-semibold text-lg mb-4">Upload Your File</h3> - <div className="bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-4"> - <div className="text-4xl mb-2">📊</div> - <p className="text-gray-600 font-medium mb-1">products.xlsx</p> - <p className="text-sm text-gray-500">1,247 rows ready</p> - </div> - <div className="flex items-center justify-center mb-4"> - <svg className="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" /> - </svg> - </div> - <div className="grid grid-cols-4 gap-2"> - {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( - <div key={i} className="aspect-square bg-gray-200 rounded flex items-center justify-center text-xs text-gray-500"> - QR {i} - </div> - ))} - </div> - <p className="text-center text-sm text-gray-600 mt-4"> - + 1,239 more codes - </p> - </Card> - <div className="absolute -top-4 -right-4 bg-green-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg"> - 1000s at Once! - </div> - </div> - </div> - </div> - </section> - - {/* Supported QR Code Types */} - <section className="py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - Supported QR Code Types - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Generate multiple types of QR codes from your CSV or Excel file. Each type has its own format requirements. - </p> - </div> - - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> - {qrCodeTypes.map((qrType, index) => ( - <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> - <div className="flex items-center space-x-3 mb-4"> - <div className="text-3xl">{qrType.icon}</div> - <div> - <h3 className="text-lg font-semibold text-gray-900"> - {qrType.title} - </h3> - <span className="text-xs font-mono text-gray-500">{qrType.type}</span> - </div> - </div> - <p className="text-gray-600 mb-4 text-sm"> - {qrType.description} - </p> - <div className="bg-gray-50 rounded-lg p-3 mb-3"> - <p className="text-xs font-semibold text-gray-700 mb-1">Format:</p> - <code className="text-xs text-gray-900 break-all">{qrType.format}</code> - </div> - <div className="bg-blue-50 rounded-lg p-3"> - <p className="text-xs font-semibold text-blue-700 mb-1">CSV Example:</p> - <code className="text-xs text-blue-900 break-all">{qrType.example}</code> - </div> - </Card> - ))} - </div> - - <div className="mt-12 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-8"> - <h3 className="text-xl font-bold text-gray-900 mb-4 text-center"> - đŸ“„ CSV/Excel File Format - </h3> - <p className="text-gray-600 text-center mb-6"> - Your file must have at least these three columns: <code className="bg-white px-2 py-1 rounded">title</code>, <code className="bg-white px-2 py-1 rounded">contentType</code>, and <code className="bg-white px-2 py-1 rounded">content</code> - </p> - <div className="bg-white rounded-lg p-6 shadow-sm overflow-x-auto"> - <table className="w-full text-sm"> - <thead> - <tr className="border-b-2 border-gray-300"> - <th className="text-left py-2 px-3 font-semibold text-gray-700">title</th> - <th className="text-left py-2 px-3 font-semibold text-gray-700">contentType</th> - <th className="text-left py-2 px-3 font-semibold text-gray-700">content</th> - <th className="text-left py-2 px-3 font-semibold text-gray-700">tags</th> - </tr> - </thead> - <tbody className="font-mono text-xs"> - <tr className="border-b border-gray-200"> - <td className="py-2 px-3">Product Page</td> - <td className="py-2 px-3">URL</td> - <td className="py-2 px-3">https://example.com/product</td> - <td className="py-2 px-3">product,shop</td> - </tr> - <tr className="border-b border-gray-200"> - <td className="py-2 px-3">John Doe</td> - <td className="py-2 px-3">VCARD</td> - <td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td> - <td className="py-2 px-3">contact</td> - </tr> - <tr className="border-b border-gray-200"> - <td className="py-2 px-3">Office Location</td> - <td className="py-2 px-3">GEO</td> - <td className="py-2 px-3">37.7749,-122.4194,Main Office</td> - <td className="py-2 px-3">location</td> - </tr> - <tr className="border-b border-gray-200"> - <td className="py-2 px-3">Support Hotline</td> - <td className="py-2 px-3">PHONE</td> - <td className="py-2 px-3">+1234567890</td> - <td className="py-2 px-3">support</td> - </tr> - <tr> - <td className="py-2 px-3">Serial Number</td> - <td className="py-2 px-3">TEXT</td> - <td className="py-2 px-3">SN-12345-ABCDE</td> - <td className="py-2 px-3">product,serial</td> - </tr> - </tbody> - </table> - </div> - </div> - </div> - </section> - - {/* Features */} - <section className="py-20 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - Powerful Bulk Generation Features - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Everything you need to create and manage QR codes at scale - </p> - </div> - - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> - {bulkFeatures.map((feature, index) => ( - <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> - <div className="text-4xl mb-4">{feature.icon}</div> - <h3 className="text-xl font-semibold text-gray-900 mb-2"> - {feature.title} - </h3> - <p className="text-gray-600"> - {feature.description} - </p> - </Card> - ))} - </div> - </div> - </section> - - {/* How It Works */} - <section className="py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - How Bulk QR Generation Works - </h2> - <p className="text-xl text-gray-600"> - Simple 4-step process to create hundreds of QR codes - </p> - </div> - - <div className="space-y-8"> - {howItWorks.map((item, index) => ( - <Card key={index} className="p-8"> - <div className="flex items-start space-x-6"> - <div className="flex-shrink-0 w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center"> - <span className="text-2xl font-bold text-primary-600">{item.step}</span> - </div> - <div className="flex-1"> - <h3 className="text-2xl font-bold text-gray-900 mb-2"> - {item.title} - </h3> - <p className="text-gray-600 mb-3"> - {item.description} - </p> - <div className="bg-blue-50 border-l-4 border-blue-500 p-3"> - <p className="text-sm text-gray-700 font-mono"> - {item.example} - </p> - </div> - </div> - </div> - </Card> - ))} - </div> - </div> - </section> - - {/* File Format Guide */} - <section className="py-20 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - CSV/Excel File Format - </h2> - <p className="text-xl text-gray-600"> - Simple file structure for bulk QR code generation - </p> - </div> - - <Card className="overflow-hidden shadow-xl mb-8"> - <table className="w-full"> - <thead className="bg-gray-100"> - <tr> - <th className="px-6 py-4 text-left text-gray-900 font-semibold">Column</th> - <th className="px-6 py-4 text-left text-gray-900 font-semibold">Description</th> - <th className="px-6 py-4 text-center text-gray-900 font-semibold">Required</th> - </tr> - </thead> - <tbody className="divide-y divide-gray-200"> - {fileFormat.map((field, index) => ( - <tr key={index}> - <td className="px-6 py-4"> - <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-900"> - {field.column} - </code> - </td> - <td className="px-6 py-4 text-gray-600">{field.description}</td> - <td className="px-6 py-4 text-center"> - {field.required ? ( - <span className="text-red-500 font-semibold">Yes</span> - ) : ( - <span className="text-gray-400">No</span> - )} - </td> - </tr> - ))} - </tbody> - </table> - </Card> - - <Card className="p-6 bg-blue-50 border border-blue-200"> - <h4 className="font-semibold text-gray-900 mb-2">Example CSV:</h4> - <pre className="bg-white p-4 rounded border border-blue-200 overflow-x-auto text-sm font-mono"> - {`name,url,description,tags -Product A,https://example.com/product-a,Premium Widget,electronics,featured -Product B,https://example.com/product-b,Standard Widget,electronics -Product C,https://example.com/product-c,Budget Widget,electronics,sale`} - </pre> - </Card> - </div> - </section> - - {/* Use Cases */} - <section className="py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - Bulk QR Code Use Cases - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Industries and scenarios where bulk generation shines - </p> - </div> - - <div className="grid md:grid-cols-2 gap-8"> - {useCases.map((useCase, index) => ( - <Card key={index} className="p-8"> - <div className="flex items-start space-x-4"> - <div className="text-4xl">{useCase.icon}</div> - <div className="flex-1"> - <h3 className="text-2xl font-bold text-gray-900 mb-3"> - {useCase.title} - </h3> - <p className="text-gray-600 mb-4"> - {useCase.description} - </p> - <ul className="space-y-2"> - {useCase.stats.map((stat, idx) => ( - <li key={idx} className="flex items-center space-x-2"> - <svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - <span className="text-gray-700">{stat}</span> - </li> - ))} - </ul> - </div> - </div> - </Card> - ))} - </div> - </div> - </section> - - {/* CTA Section */} - <section className="py-20 bg-gradient-to-r from-green-600 to-blue-600 text-white"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> - <h2 className="text-4xl font-bold mb-6"> - Generate 1000s of QR Codes in Minutes - </h2> - <p className="text-xl mb-8 text-green-100"> - Save hours of manual work. Upload your file and get all QR codes ready instantly. - </p> - <div className="flex flex-col sm:flex-row gap-4 justify-center"> - <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"> - Start Bulk Generation - </Button> - </Link> - <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"> - View Pricing - </Button> - </Link> - </div> - </div> - </section> - </div> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; +import { Card } from '@/components/ui/Card'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; +import { breadcrumbSchema } from '@/lib/schema'; + +export const metadata: Metadata = { + title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel | QR Master', + description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.', + keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free', + alternates: { + canonical: 'https://www.qrmaster.net/bulk-qr-code-generator', + languages: { + 'x-default': 'https://www.qrmaster.net/bulk-qr-code-generator', + en: 'https://www.qrmaster.net/bulk-qr-code-generator', + }, + }, + openGraph: { + title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel', + description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.', + url: 'https://www.qrmaster.net/bulk-qr-code-generator', + type: 'website', + }, + twitter: { + title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel', + description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.', + }, +}; + +export default function BulkQRCodeGeneratorPage() { + const qrCodeTypes = [ + { + type: 'URL', + icon: '🌐', + title: 'Website Links', + description: 'Generate QR codes for websites, landing pages, and online content', + format: 'https://example.com/product', + example: 'Product Page,URL,https://example.com/product', + }, + { + type: 'VCARD', + icon: 'đŸ‘€', + title: 'Contact Cards', + description: 'Create vCard QR codes with contact information', + format: 'FirstName,LastName,Email,Phone,Organization,Title', + example: 'John Doe,VCARD,John,Doe,john@example.com,+1234567890,Company Inc,CEO', + }, + { + type: 'GEO', + icon: '📍', + title: 'Locations', + description: 'Generate location QR codes with GPS coordinates', + format: 'latitude,longitude,label', + example: 'Office Location,GEO,37.7749,-122.4194,Main Office', + }, + { + type: 'PHONE', + icon: '📞', + title: 'Phone Numbers', + description: 'Create QR codes that dial phone numbers', + format: '+1234567890', + example: 'Support Hotline,PHONE,+1234567890', + }, + { + type: 'TEXT', + icon: '📝', + title: 'Plain Text', + description: 'Generate QR codes with any text content', + format: 'Your text content here', + example: 'Serial Number,TEXT,SN-12345-ABCDE', + }, + ]; + + const bulkFeatures = [ + { + icon: '📊', + title: 'Excel & CSV Import', + description: 'Upload Excel or CSV files to generate hundreds of QR codes in seconds. Simple column mapping.', + }, + { + icon: '⚡', + title: 'Fast Processing', + description: 'Generate up to 1000 QR codes in under a minute. Optimized for speed and reliability.', + }, + { + icon: '🎹', + title: 'Unified Branding', + description: 'Apply your logo, colors, and design to all QR codes at once. Consistent brand identity.', + }, + { + icon: '📩', + title: 'Batch Download', + description: 'Download all QR codes as a ZIP file with custom filenames. Organized and ready to use.', + }, + { + icon: '📈', + title: 'Individual Tracking', + description: 'Track each QR code separately. See which products or locations perform best.', + }, + { + icon: '🔄', + title: 'Update in Bulk', + description: 'Edit multiple QR codes at once. Save time when updating campaigns or product info.', + }, + ]; + + const useCases = [ + { + title: 'Product Labels', + icon: 'đŸ·ïž', + description: 'Generate unique QR codes for each product SKU. Link to manuals, warranty info, or product pages.', + stats: ['1000+ products', 'Individual tracking', 'Custom naming'], + }, + { + title: 'Event Tickets', + icon: 'đŸŽŸïž', + description: 'Create unique QR codes for every attendee. Enable fast check-ins and track attendance.', + stats: ['Unique per ticket', 'Real-time validation', 'Analytics dashboard'], + }, + { + title: 'Asset Management', + icon: 'đŸ’Œ', + description: 'Tag equipment, furniture, and assets with unique QR codes. Track location and maintenance.', + stats: ['Equipment tracking', 'Maintenance logs', 'Location history'], + }, + { + title: 'Marketing Campaigns', + icon: '📱', + description: 'Generate codes for different locations or channels. Track which campaigns perform best.', + stats: ['Location-specific', 'Campaign tracking', 'ROI measurement'], + }, + ]; + + const howItWorks = [ + { + step: 1, + title: 'Prepare Your File', + description: 'Create an Excel or CSV file with your URLs, names, and any custom data.', + example: 'Product Name | URL | SKU', + }, + { + step: 2, + title: 'Upload & Map', + description: 'Upload your file and map columns to QR code fields. Preview before generating.', + example: 'Map columns: Name → Title, URL → Destination', + }, + { + step: 3, + title: 'Customize Design', + description: 'Apply logo, colors, and branding to all QR codes at once. Consistent look.', + example: 'Add logo, set colors, choose frame', + }, + { + step: 4, + title: 'Generate & Download', + description: 'Click generate and download all QR codes as PNG files in a ZIP archive.', + example: 'product-001.png, product-002.png, ...', + }, + ]; + + const fileFormat = [ + { column: 'name', description: 'QR code title/label', required: true }, + { column: 'url', description: 'Destination URL', required: true }, + { column: 'description', description: 'Optional description', required: false }, + { column: 'tags', description: 'Comma-separated tags', required: false }, + ]; + + const softwareSchema = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#software', + name: 'QR Master - Bulk QR Code Generator', + applicationCategory: 'BusinessApplication', + operatingSystem: 'Web Browser', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + availability: 'https://schema.org/InStock', + }, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.8', + ratingCount: '980', + }, + description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, inventory management with custom branding.', + featureList: [ + 'Excel and CSV file import', + 'Generate up to 1000 QR codes at once', + 'Unified branding and design', + 'Batch download as ZIP', + 'Individual tracking per code', + 'Bulk update capabilities', + 'Custom filenames', + 'High-resolution exports', + ], + }; + + const howToSchema = { + '@context': 'https://schema.org', + '@type': 'HowTo', + '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#howto', + name: 'How to Generate Bulk QR Codes from Excel', + description: 'Learn how to create hundreds of QR codes from an Excel or CSV file', + totalTime: 'PT10M', + step: [ + { + '@type': 'HowToStep', + position: 1, + name: 'Prepare Excel File', + text: 'Create an Excel or CSV file with columns for name, URL, and any custom data you need', + }, + { + '@type': 'HowToStep', + position: 2, + name: 'Upload File', + text: 'Log into QR Master and upload your file to the bulk generator', + url: 'https://www.qrmaster.net/bulk-creation', + }, + { + '@type': 'HowToStep', + position: 3, + name: 'Map Columns', + text: 'Map your file columns to QR code fields (name, URL, description, etc.)', + }, + { + '@type': 'HowToStep', + position: 4, + name: 'Customize Design', + text: 'Apply your logo, brand colors, and design settings to all QR codes at once', + }, + { + '@type': 'HowToStep', + position: 5, + name: 'Generate and Download', + text: 'Click generate and download all QR codes as a ZIP file with custom filenames', + }, + ], + }; + + const faqSchema = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#faq', + mainEntity: [ + { + '@type': 'Question', + name: 'How many QR codes can I generate at once?', + acceptedAnswer: { + '@type': 'Answer', + text: 'With QR Master, you can generate up to 1000 QR codes at once from a CSV or Excel file. For larger volumes, contact our enterprise team.', + }, + }, + { + '@type': 'Question', + name: 'What file formats are supported?', + acceptedAnswer: { + '@type': 'Answer', + text: 'QR Master supports CSV (.csv), Excel (.xlsx, .xls), and other spreadsheet formats. Simply ensure your file has columns for name and destination URL.', + }, + }, + { + '@type': 'Question', + name: 'Can I apply my branding to all QR codes?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Yes, you can apply your logo, brand colors, and custom design to all QR codes in your bulk generation. All codes will have consistent branding.', + }, + }, + { + '@type': 'Question', + name: 'Are bulk generated QR codes trackable?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Yes, each QR code generated in bulk is individually trackable. You can see scans, locations, and analytics for every single code in your dashboard.', + }, + }, + ], + }; + + const breadcrumbItems: BreadcrumbItem[] = [ + { name: 'Home', url: '/' }, + { name: 'Bulk QR Code Generator', url: '/bulk-qr-code-generator' }, + ]; + + return ( + <> + <SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} /> + <div className="min-h-screen bg-white"> + {/* Hero Section */} + <section className="relative overflow-hidden bg-gradient-to-br from-green-50 via-white to-blue-50 py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <Breadcrumbs items={breadcrumbItems} /> + <div className="grid lg:grid-cols-2 gap-12 items-center"> + <div className="space-y-8"> + <div className="inline-flex items-center space-x-2 bg-green-100 text-green-800 px-4 py-2 rounded-full text-sm font-semibold"> + <span>⚡</span> + <span>Generate 1000s in Minutes</span> + </div> + + <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> + Bulk QR Code Generator + </h1> + + <p className="text-xl text-gray-600 leading-relaxed"> + Create hundreds or thousands of QR codes from Excel or CSV files. Generate URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory, and marketing campaigns. + </p> + + <div className="space-y-3"> + {[ + 'Upload Excel or CSV files', + 'Generate URLs, vCards, locations & more', + 'Custom branding on all codes', + 'Download as organized ZIP', + ].map((feature, index) => ( + <div key={index} className="flex items-center space-x-3"> + <div className="flex-shrink-0 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"> + <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + </div> + <span className="text-gray-700">{feature}</span> + </div> + ))} + </div> + + <div className="flex flex-col sm:flex-row gap-4"> + <Link href="/signup"> + <Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> + Start Bulk Generation + </Button> + </Link> + <Link href="/create"> + <Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> + Try Single QR First + </Button> + </Link> + </div> + </div> + + {/* Visual Example */} + <div className="relative"> + <Card className="p-6 shadow-2xl"> + <h3 className="font-semibold text-lg mb-4">Upload Your File</h3> + <div className="bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-4"> + <div className="text-4xl mb-2">📊</div> + <p className="text-gray-600 font-medium mb-1">products.xlsx</p> + <p className="text-sm text-gray-500">1,247 rows ready</p> + </div> + <div className="flex items-center justify-center mb-4"> + <svg className="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" /> + </svg> + </div> + <div className="grid grid-cols-4 gap-2"> + {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( + <div key={i} className="aspect-square bg-gray-200 rounded flex items-center justify-center text-xs text-gray-500"> + QR {i} + </div> + ))} + </div> + <p className="text-center text-sm text-gray-600 mt-4"> + + 1,239 more codes + </p> + </Card> + <div className="absolute -top-4 -right-4 bg-green-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg"> + 1000s at Once! + </div> + </div> + </div> + </div> + </section> + + {/* Supported QR Code Types */} + <section className="py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + Supported QR Code Types + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Generate multiple types of QR codes from your CSV or Excel file. Each type has its own format requirements. + </p> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> + {qrCodeTypes.map((qrType, index) => ( + <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> + <div className="flex items-center space-x-3 mb-4"> + <div className="text-3xl">{qrType.icon}</div> + <div> + <h3 className="text-lg font-semibold text-gray-900"> + {qrType.title} + </h3> + <span className="text-xs font-mono text-gray-500">{qrType.type}</span> + </div> + </div> + <p className="text-gray-600 mb-4 text-sm"> + {qrType.description} + </p> + <div className="bg-gray-50 rounded-lg p-3 mb-3"> + <p className="text-xs font-semibold text-gray-700 mb-1">Format:</p> + <code className="text-xs text-gray-900 break-all">{qrType.format}</code> + </div> + <div className="bg-blue-50 rounded-lg p-3"> + <p className="text-xs font-semibold text-blue-700 mb-1">CSV Example:</p> + <code className="text-xs text-blue-900 break-all">{qrType.example}</code> + </div> + </Card> + ))} + </div> + + <div className="mt-12 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-8"> + <h3 className="text-xl font-bold text-gray-900 mb-4 text-center"> + đŸ“„ CSV/Excel File Format + </h3> + <p className="text-gray-600 text-center mb-6"> + Your file must have at least these three columns: <code className="bg-white px-2 py-1 rounded">title</code>, <code className="bg-white px-2 py-1 rounded">contentType</code>, and <code className="bg-white px-2 py-1 rounded">content</code> + </p> + <div className="bg-white rounded-lg p-6 shadow-sm overflow-x-auto"> + <table className="w-full text-sm"> + <thead> + <tr className="border-b-2 border-gray-300"> + <th className="text-left py-2 px-3 font-semibold text-gray-700">title</th> + <th className="text-left py-2 px-3 font-semibold text-gray-700">contentType</th> + <th className="text-left py-2 px-3 font-semibold text-gray-700">content</th> + <th className="text-left py-2 px-3 font-semibold text-gray-700">tags</th> + </tr> + </thead> + <tbody className="font-mono text-xs"> + <tr className="border-b border-gray-200"> + <td className="py-2 px-3">Product Page</td> + <td className="py-2 px-3">URL</td> + <td className="py-2 px-3">https://example.com/product</td> + <td className="py-2 px-3">product,shop</td> + </tr> + <tr className="border-b border-gray-200"> + <td className="py-2 px-3">John Doe</td> + <td className="py-2 px-3">VCARD</td> + <td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td> + <td className="py-2 px-3">contact</td> + </tr> + <tr className="border-b border-gray-200"> + <td className="py-2 px-3">Office Location</td> + <td className="py-2 px-3">GEO</td> + <td className="py-2 px-3">37.7749,-122.4194,Main Office</td> + <td className="py-2 px-3">location</td> + </tr> + <tr className="border-b border-gray-200"> + <td className="py-2 px-3">Support Hotline</td> + <td className="py-2 px-3">PHONE</td> + <td className="py-2 px-3">+1234567890</td> + <td className="py-2 px-3">support</td> + </tr> + <tr> + <td className="py-2 px-3">Serial Number</td> + <td className="py-2 px-3">TEXT</td> + <td className="py-2 px-3">SN-12345-ABCDE</td> + <td className="py-2 px-3">product,serial</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </section> + + {/* Features */} + <section className="py-20 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + Powerful Bulk Generation Features + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Everything you need to create and manage QR codes at scale + </p> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> + {bulkFeatures.map((feature, index) => ( + <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> + <div className="text-4xl mb-4">{feature.icon}</div> + <h3 className="text-xl font-semibold text-gray-900 mb-2"> + {feature.title} + </h3> + <p className="text-gray-600"> + {feature.description} + </p> + </Card> + ))} + </div> + </div> + </section> + + {/* How It Works */} + <section className="py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + How Bulk QR Generation Works + </h2> + <p className="text-xl text-gray-600"> + Simple 4-step process to create hundreds of QR codes + </p> + </div> + + <div className="space-y-8"> + {howItWorks.map((item, index) => ( + <Card key={index} className="p-8"> + <div className="flex items-start space-x-6"> + <div className="flex-shrink-0 w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center"> + <span className="text-2xl font-bold text-primary-600">{item.step}</span> + </div> + <div className="flex-1"> + <h3 className="text-2xl font-bold text-gray-900 mb-2"> + {item.title} + </h3> + <p className="text-gray-600 mb-3"> + {item.description} + </p> + <div className="bg-blue-50 border-l-4 border-blue-500 p-3"> + <p className="text-sm text-gray-700 font-mono"> + {item.example} + </p> + </div> + </div> + </div> + </Card> + ))} + </div> + </div> + </section> + + {/* File Format Guide */} + <section className="py-20 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + CSV/Excel File Format + </h2> + <p className="text-xl text-gray-600"> + Simple file structure for bulk QR code generation + </p> + </div> + + <Card className="overflow-hidden shadow-xl mb-8"> + <table className="w-full"> + <thead className="bg-gray-100"> + <tr> + <th className="px-6 py-4 text-left text-gray-900 font-semibold">Column</th> + <th className="px-6 py-4 text-left text-gray-900 font-semibold">Description</th> + <th className="px-6 py-4 text-center text-gray-900 font-semibold">Required</th> + </tr> + </thead> + <tbody className="divide-y divide-gray-200"> + {fileFormat.map((field, index) => ( + <tr key={index}> + <td className="px-6 py-4"> + <code className="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-900"> + {field.column} + </code> + </td> + <td className="px-6 py-4 text-gray-600">{field.description}</td> + <td className="px-6 py-4 text-center"> + {field.required ? ( + <span className="text-red-500 font-semibold">Yes</span> + ) : ( + <span className="text-gray-400">No</span> + )} + </td> + </tr> + ))} + </tbody> + </table> + </Card> + + <Card className="p-6 bg-blue-50 border border-blue-200"> + <h4 className="font-semibold text-gray-900 mb-2">Example CSV:</h4> + <pre className="bg-white p-4 rounded border border-blue-200 overflow-x-auto text-sm font-mono"> + {`name,url,description,tags +Product A,https://example.com/product-a,Premium Widget,electronics,featured +Product B,https://example.com/product-b,Standard Widget,electronics +Product C,https://example.com/product-c,Budget Widget,electronics,sale`} + </pre> + </Card> + </div> + </section> + + {/* Use Cases */} + <section className="py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + Bulk QR Code Use Cases + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Industries and scenarios where bulk generation shines + </p> + </div> + + <div className="grid md:grid-cols-2 gap-8"> + {useCases.map((useCase, index) => ( + <Card key={index} className="p-8"> + <div className="flex items-start space-x-4"> + <div className="text-4xl">{useCase.icon}</div> + <div className="flex-1"> + <h3 className="text-2xl font-bold text-gray-900 mb-3"> + {useCase.title} + </h3> + <p className="text-gray-600 mb-4"> + {useCase.description} + </p> + <ul className="space-y-2"> + {useCase.stats.map((stat, idx) => ( + <li key={idx} className="flex items-center space-x-2"> + <svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + <span className="text-gray-700">{stat}</span> + </li> + ))} + </ul> + </div> + </div> + </Card> + ))} + </div> + </div> + </section> + + {/* CTA Section */} + <section className="py-20 bg-gradient-to-r from-green-600 to-blue-600 text-white"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> + <h2 className="text-4xl font-bold mb-6"> + Generate 1000s of QR Codes in Minutes + </h2> + <p className="text-xl mb-8 text-green-100"> + Save hours of manual work. Upload your file and get all QR codes ready instantly. + </p> + <div className="flex flex-col sm:flex-row gap-4 justify-center"> + <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"> + Start Bulk Generation + </Button> + </Link> + <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"> + View Pricing + </Button> + </Link> + </div> + </div> + </section> + </div> + </> + ); +} diff --git a/src/app/(marketing)/dynamic-qr-code-generator/page.tsx b/src/app/(marketing)/dynamic-qr-code-generator/page.tsx index 0cb0a55..69cdba1 100644 --- a/src/app/(marketing)/dynamic-qr-code-generator/page.tsx +++ b/src/app/(marketing)/dynamic-qr-code-generator/page.tsx @@ -1,518 +1,518 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import Link from 'next/link'; -import { Button } from '@/components/ui/Button'; -import { Card } from '@/components/ui/Card'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; -import { breadcrumbSchema } from '@/lib/schema'; - -export const metadata: Metadata = { - title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master', - description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.', - keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator', - alternates: { - canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator', - languages: { - 'x-default': 'https://www.qrmaster.net/dynamic-qr-code-generator', - en: 'https://www.qrmaster.net/dynamic-qr-code-generator', - }, - }, - openGraph: { - title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master', - description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.', - url: 'https://www.qrmaster.net/dynamic-qr-code-generator', - type: 'website', - }, - twitter: { - title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master', - description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.', - }, -}; - -export default function DynamicQRCodeGeneratorPage() { - const dynamicFeatures = [ - { - icon: '✏', - title: 'Edit Anytime', - description: 'Change the destination URL or content after your QR code is printed. No need to reprint!', - }, - { - icon: '📊', - title: 'Advanced Analytics', - description: 'Track scans, locations, devices, and time patterns. Get insights to optimize your campaigns.', - }, - { - icon: '🎹', - title: 'Full Customization', - description: 'Add your logo, brand colors, custom shapes, and frames. Make your QR code stand out.', - }, - { - icon: '🔄', - title: 'A/B Testing', - description: 'Test different landing pages without changing the QR code. Optimize conversions easily.', - }, - { - icon: '⏰', - title: 'Schedule Content', - description: 'Set time-based redirects. Show different content based on day, time, or season.', - }, - { - icon: '🌍', - title: 'Geo-Targeting', - description: 'Redirect users to different pages based on their location. Perfect for multi-region campaigns.', - }, - ]; - - const staticVsDynamic = [ - { - feature: 'Edit After Printing', - static: false, - dynamic: true, - }, - { - feature: 'Track Scans', - static: false, - dynamic: true, - }, - { - feature: 'A/B Testing', - static: false, - dynamic: true, - }, - { - feature: 'Analytics Dashboard', - static: false, - dynamic: true, - }, - { - feature: 'Custom Domain', - static: false, - dynamic: true, - }, - { - feature: 'Password Protection', - static: false, - dynamic: true, - }, - { - feature: 'Expiration Date', - static: false, - dynamic: true, - }, - ]; - - const useCases = [ - { - title: 'Marketing Campaigns', - icon: '📱', - description: 'Update campaign landing pages without reprinting materials. Test different offers and track performance.', - example: 'Print QR codes on billboards, then test different promotions weekly.', - }, - { - title: 'Product Packaging', - icon: '📩', - description: 'Link to product manuals, videos, or registration forms. Update information as products evolve.', - example: 'Update software download links without changing packaging.', - }, - { - title: 'Business Cards', - icon: 'đŸ’Œ', - description: 'Keep your contact information current. Update your vCard details without printing new cards.', - example: 'Change job title, phone, or email anytime.', - }, - { - title: 'Restaurant Menus', - icon: 'đŸœïž', - description: 'Update menu items, prices, and specials daily. Track which items get the most views.', - example: 'Show daily specials without printing new menus.', - }, - ]; - - const softwareSchema = { - '@context': 'https://schema.org', - '@type': 'SoftwareApplication', - '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#software', - name: 'QR Master - Dynamic QR Code Generator', - applicationCategory: 'BusinessApplication', - operatingSystem: 'Web Browser', - offers: { - '@type': 'Offer', - price: '0', - priceCurrency: 'USD', - availability: 'https://schema.org/InStock', - }, - aggregateRating: { - '@type': 'AggregateRating', - ratingValue: '4.9', - ratingCount: '2150', - }, - description: 'Create dynamic QR codes that can be edited after printing. Change destination URLs, track scans, and update content without reprinting.', - featureList: [ - 'Edit QR codes after printing', - 'Real-time scan tracking', - 'A/B testing capabilities', - 'Custom branding and design', - 'Geo-targeting options', - 'Scheduled content updates', - 'Password protection', - 'Expiration dates', - ], - }; - - const howToSchema = { - '@context': 'https://schema.org', - '@type': 'HowTo', - '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#howto', - name: 'How to Create a Dynamic QR Code', - description: 'Learn how to create editable QR codes that can be updated after printing', - totalTime: 'PT3M', - step: [ - { - '@type': 'HowToStep', - position: 1, - name: 'Sign Up Free', - text: 'Create a free QR Master account to start generating dynamic QR codes', - url: 'https://www.qrmaster.net/signup', - }, - { - '@type': 'HowToStep', - position: 2, - name: 'Generate QR Code', - text: 'Enter your destination URL and customize the design with your branding', - url: 'https://www.qrmaster.net/create', - }, - { - '@type': 'HowToStep', - position: 3, - name: 'Download and Print', - text: 'Download your QR code in high resolution and add it to your marketing materials', - }, - { - '@type': 'HowToStep', - position: 4, - name: 'Update Anytime', - text: 'Log into your dashboard to change the destination URL whenever needed - no reprinting required', - url: 'https://www.qrmaster.net/dashboard', - }, - ], - }; - - const faqSchema = { - '@context': 'https://schema.org', - '@type': 'FAQPage', - '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#faq', - mainEntity: [ - { - '@type': 'Question', - name: 'What is a dynamic QR code?', - acceptedAnswer: { - '@type': 'Answer', - text: 'A dynamic QR code is an editable QR code that redirects through a short URL, allowing you to change the destination without reprinting the code. Unlike static QR codes, dynamic codes can be tracked and updated anytime.', - }, - }, - { - '@type': 'Question', - name: 'Can I edit a QR code after printing?', - acceptedAnswer: { - '@type': 'Answer', - text: 'Yes, with dynamic QR codes you can edit the destination URL anytime after printing. The QR code image stays the same, but the content it points to can be changed from your dashboard.', - }, - }, - { - '@type': 'Question', - name: 'Is dynamic QR code generator free?', - acceptedAnswer: { - '@type': 'Answer', - text: 'Yes, QR Master offers a free plan for creating dynamic QR codes with basic tracking features. Premium plans include advanced analytics and customization options.', - }, - }, - ], - }; - - const breadcrumbItems: BreadcrumbItem[] = [ - { name: 'Home', url: '/' }, - { name: 'Dynamic QR Code Generator', url: '/dynamic-qr-code-generator' }, - ]; - - return ( - <> - <SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} /> - <div className="min-h-screen bg-white"> - {/* Hero Section */} - <section className="relative overflow-hidden bg-gradient-to-br from-purple-50 via-white to-blue-50 py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <Breadcrumbs items={breadcrumbItems} /> - <div className="grid lg:grid-cols-2 gap-12 items-center"> - <div className="space-y-8"> - <div className="inline-flex items-center space-x-2 bg-purple-100 text-purple-800 px-4 py-2 rounded-full text-sm font-semibold"> - <span>✹</span> - <span>Edit After Printing</span> - </div> - - <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> - Dynamic QR Code Generator - </h1> - - <p className="text-xl text-gray-600 leading-relaxed"> - Create QR codes you can edit anytime - even after printing. Change URLs, track scans, and update content without reprinting. The smart choice for businesses. - </p> - - <div className="space-y-3"> - {[ - 'Edit content after printing', - 'Track scans and analytics', - 'A/B test without reprinting', - 'Custom branding and design', - ].map((feature, index) => ( - <div key={index} className="flex items-center space-x-3"> - <div className="flex-shrink-0 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"> - <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - </div> - <span className="text-gray-700">{feature}</span> - </div> - ))} - </div> - - <div className="flex flex-col sm:flex-row gap-4"> - <Link href="/signup"> - <Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> - Create Dynamic QR Code - </Button> - </Link> - <Link href="/pricing"> - <Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> - View Pricing - </Button> - </Link> - </div> - </div> - - {/* Visual Demo */} - <div className="relative"> - <Card className="p-8 shadow-2xl"> - <div className="text-center mb-6"> - <div className="inline-block bg-gray-200 rounded-lg p-8"> - <div className="w-48 h-48 bg-black rounded-lg flex items-center justify-center"> - <span className="text-white text-sm font-mono">QR Code</span> - </div> - </div> - </div> - <div className="space-y-3 text-sm"> - <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"> - <span className="text-gray-700">Current URL:</span> - <span className="text-blue-600 font-mono">summer-sale.com</span> - </div> - <div className="flex items-center justify-center"> - <svg className="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" /> - </svg> - </div> - <div className="flex items-center justify-between p-3 bg-green-50 rounded-lg"> - <span className="text-gray-700">Updated URL:</span> - <span className="text-green-600 font-mono">fall-sale.com</span> - </div> - </div> - <p className="text-center text-sm text-gray-600 mt-4"> - Same QR code, different destination! - </p> - </Card> - <div className="absolute -top-4 -right-4 bg-purple-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg"> - No Reprint Needed! - </div> - </div> - </div> - </div> - </section> - - {/* Static vs Dynamic */} - <section className="py-20 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - Dynamic vs Static QR Codes - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Understand why dynamic QR codes are the smart choice for businesses - </p> - </div> - - <Card className="overflow-hidden shadow-xl"> - <div className="grid md:grid-cols-3"> - <div className="p-6 bg-white"> - <h3 className="font-semibold text-lg mb-4">Feature</h3> - {staticVsDynamic.map((item, index) => ( - <div key={index} className="py-4 border-b last:border-b-0"> - <p className="text-gray-900 font-medium">{item.feature}</p> - </div> - ))} - </div> - <div className="p-6 bg-gray-50"> - <h3 className="font-semibold text-lg mb-4 text-gray-600">Static QR</h3> - {staticVsDynamic.map((item, index) => ( - <div key={index} className="py-4 border-b last:border-b-0 flex items-center justify-center"> - {item.static ? ( - <span className="text-green-500 text-2xl">✓</span> - ) : ( - <span className="text-red-500 text-2xl">✗</span> - )} - </div> - ))} - </div> - <div className="p-6 bg-primary-50"> - <h3 className="font-semibold text-lg mb-4 text-primary-600">Dynamic QR</h3> - {staticVsDynamic.map((item, index) => ( - <div key={index} className="py-4 border-b last:border-b-0 flex items-center justify-center"> - {item.dynamic ? ( - <span className="text-green-500 text-2xl">✓</span> - ) : ( - <span className="text-red-500 text-2xl">✗</span> - )} - </div> - ))} - </div> - </div> - </Card> - </div> - </section> - - {/* Features */} - <section className="py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - Powerful Dynamic QR Features - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Everything you need to create, manage, and optimize your QR code campaigns - </p> - </div> - - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> - {dynamicFeatures.map((feature, index) => ( - <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> - <div className="text-4xl mb-4">{feature.icon}</div> - <h3 className="text-xl font-semibold text-gray-900 mb-2"> - {feature.title} - </h3> - <p className="text-gray-600"> - {feature.description} - </p> - </Card> - ))} - </div> - </div> - </section> - - {/* Use Cases */} - <section className="py-20 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - How Businesses Use Dynamic QR Codes - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Real-world examples of dynamic QR code applications - </p> - </div> - - <div className="grid md:grid-cols-2 gap-8"> - {useCases.map((useCase, index) => ( - <Card key={index} className="p-8"> - <div className="flex items-start space-x-4"> - <div className="text-4xl">{useCase.icon}</div> - <div className="flex-1"> - <h3 className="text-2xl font-bold text-gray-900 mb-3"> - {useCase.title} - </h3> - <p className="text-gray-600 mb-4"> - {useCase.description} - </p> - <div className="bg-blue-50 border-l-4 border-blue-500 p-4"> - <p className="text-sm text-gray-700"> - <strong>Example:</strong> {useCase.example} - </p> - </div> - </div> - </div> - </Card> - ))} - </div> - </div> - </section> - - {/* How It Works */} - <section className="py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - How Dynamic QR Codes Work - </h2> - <p className="text-xl text-gray-600"> - Simple technology, powerful results - </p> - </div> - - <div className="grid md:grid-cols-3 gap-8"> - <Card className="p-6 text-center"> - <div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4"> - <span className="text-2xl font-bold text-primary-600">1</span> - </div> - <h3 className="text-xl font-semibold text-gray-900 mb-2">Create QR Code</h3> - <p className="text-gray-600"> - Generate a dynamic QR code with a short redirect URL - </p> - </Card> - - <Card className="p-6 text-center"> - <div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4"> - <span className="text-2xl font-bold text-primary-600">2</span> - </div> - <h3 className="text-xl font-semibold text-gray-900 mb-2">Print Anywhere</h3> - <p className="text-gray-600"> - Add to packaging, posters, cards, or anywhere you need - </p> - </Card> - - <Card className="p-6 text-center"> - <div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4"> - <span className="text-2xl font-bold text-primary-600">3</span> - </div> - <h3 className="text-xl font-semibold text-gray-900 mb-2">Update Anytime</h3> - <p className="text-gray-600"> - Change the destination URL from your dashboard whenever needed - </p> - </Card> - </div> - </div> - </section> - - {/* CTA Section */} - <section className="py-20 bg-gradient-to-r from-purple-600 to-blue-600 text-white"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> - <h2 className="text-4xl font-bold mb-6"> - Start Creating Dynamic QR Codes Today - </h2> - <p className="text-xl mb-8 text-purple-100"> - Join thousands of businesses who never worry about reprinting QR codes again - </p> - <div className="flex flex-col sm:flex-row gap-4 justify-center"> - <Link href="/signup"> - <Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-purple-600 hover:bg-gray-100"> - Get Started Free - </Button> - </Link> - <Link href="/create"> - <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"> - Create QR Code Now - </Button> - </Link> - </div> - </div> - </section> - </div> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; +import { Card } from '@/components/ui/Card'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; +import { breadcrumbSchema } from '@/lib/schema'; + +export const metadata: Metadata = { + title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master', + description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.', + keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator', + alternates: { + canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator', + languages: { + 'x-default': 'https://www.qrmaster.net/dynamic-qr-code-generator', + en: 'https://www.qrmaster.net/dynamic-qr-code-generator', + }, + }, + openGraph: { + title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master', + description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.', + url: 'https://www.qrmaster.net/dynamic-qr-code-generator', + type: 'website', + }, + twitter: { + title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master', + description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.', + }, +}; + +export default function DynamicQRCodeGeneratorPage() { + const dynamicFeatures = [ + { + icon: '✏', + title: 'Edit Anytime', + description: 'Change the destination URL or content after your QR code is printed. No need to reprint!', + }, + { + icon: '📊', + title: 'Advanced Analytics', + description: 'Track scans, locations, devices, and time patterns. Get insights to optimize your campaigns.', + }, + { + icon: '🎹', + title: 'Full Customization', + description: 'Add your logo, brand colors, custom shapes, and frames. Make your QR code stand out.', + }, + { + icon: '🔄', + title: 'A/B Testing', + description: 'Test different landing pages without changing the QR code. Optimize conversions easily.', + }, + { + icon: '⏰', + title: 'Schedule Content', + description: 'Set time-based redirects. Show different content based on day, time, or season.', + }, + { + icon: '🌍', + title: 'Geo-Targeting', + description: 'Redirect users to different pages based on their location. Perfect for multi-region campaigns.', + }, + ]; + + const staticVsDynamic = [ + { + feature: 'Edit After Printing', + static: false, + dynamic: true, + }, + { + feature: 'Track Scans', + static: false, + dynamic: true, + }, + { + feature: 'A/B Testing', + static: false, + dynamic: true, + }, + { + feature: 'Analytics Dashboard', + static: false, + dynamic: true, + }, + { + feature: 'Custom Domain', + static: false, + dynamic: true, + }, + { + feature: 'Password Protection', + static: false, + dynamic: true, + }, + { + feature: 'Expiration Date', + static: false, + dynamic: true, + }, + ]; + + const useCases = [ + { + title: 'Marketing Campaigns', + icon: '📱', + description: 'Update campaign landing pages without reprinting materials. Test different offers and track performance.', + example: 'Print QR codes on billboards, then test different promotions weekly.', + }, + { + title: 'Product Packaging', + icon: '📩', + description: 'Link to product manuals, videos, or registration forms. Update information as products evolve.', + example: 'Update software download links without changing packaging.', + }, + { + title: 'Business Cards', + icon: 'đŸ’Œ', + description: 'Keep your contact information current. Update your vCard details without printing new cards.', + example: 'Change job title, phone, or email anytime.', + }, + { + title: 'Restaurant Menus', + icon: 'đŸœïž', + description: 'Update menu items, prices, and specials daily. Track which items get the most views.', + example: 'Show daily specials without printing new menus.', + }, + ]; + + const softwareSchema = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#software', + name: 'QR Master - Dynamic QR Code Generator', + applicationCategory: 'BusinessApplication', + operatingSystem: 'Web Browser', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + availability: 'https://schema.org/InStock', + }, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.9', + ratingCount: '2150', + }, + description: 'Create dynamic QR codes that can be edited after printing. Change destination URLs, track scans, and update content without reprinting.', + featureList: [ + 'Edit QR codes after printing', + 'Real-time scan tracking', + 'A/B testing capabilities', + 'Custom branding and design', + 'Geo-targeting options', + 'Scheduled content updates', + 'Password protection', + 'Expiration dates', + ], + }; + + const howToSchema = { + '@context': 'https://schema.org', + '@type': 'HowTo', + '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#howto', + name: 'How to Create a Dynamic QR Code', + description: 'Learn how to create editable QR codes that can be updated after printing', + totalTime: 'PT3M', + step: [ + { + '@type': 'HowToStep', + position: 1, + name: 'Sign Up Free', + text: 'Create a free QR Master account to start generating dynamic QR codes', + url: 'https://www.qrmaster.net/signup', + }, + { + '@type': 'HowToStep', + position: 2, + name: 'Generate QR Code', + text: 'Enter your destination URL and customize the design with your branding', + url: 'https://www.qrmaster.net/create', + }, + { + '@type': 'HowToStep', + position: 3, + name: 'Download and Print', + text: 'Download your QR code in high resolution and add it to your marketing materials', + }, + { + '@type': 'HowToStep', + position: 4, + name: 'Update Anytime', + text: 'Log into your dashboard to change the destination URL whenever needed - no reprinting required', + url: 'https://www.qrmaster.net/dashboard', + }, + ], + }; + + const faqSchema = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#faq', + mainEntity: [ + { + '@type': 'Question', + name: 'What is a dynamic QR code?', + acceptedAnswer: { + '@type': 'Answer', + text: 'A dynamic QR code is an editable QR code that redirects through a short URL, allowing you to change the destination without reprinting the code. Unlike static QR codes, dynamic codes can be tracked and updated anytime.', + }, + }, + { + '@type': 'Question', + name: 'Can I edit a QR code after printing?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Yes, with dynamic QR codes you can edit the destination URL anytime after printing. The QR code image stays the same, but the content it points to can be changed from your dashboard.', + }, + }, + { + '@type': 'Question', + name: 'Is dynamic QR code generator free?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Yes, QR Master offers a free plan for creating dynamic QR codes with basic tracking features. Premium plans include advanced analytics and customization options.', + }, + }, + ], + }; + + const breadcrumbItems: BreadcrumbItem[] = [ + { name: 'Home', url: '/' }, + { name: 'Dynamic QR Code Generator', url: '/dynamic-qr-code-generator' }, + ]; + + return ( + <> + <SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} /> + <div className="min-h-screen bg-white"> + {/* Hero Section */} + <section className="relative overflow-hidden bg-gradient-to-br from-purple-50 via-white to-blue-50 py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <Breadcrumbs items={breadcrumbItems} /> + <div className="grid lg:grid-cols-2 gap-12 items-center"> + <div className="space-y-8"> + <div className="inline-flex items-center space-x-2 bg-purple-100 text-purple-800 px-4 py-2 rounded-full text-sm font-semibold"> + <span>✹</span> + <span>Edit After Printing</span> + </div> + + <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> + Dynamic QR Code Generator + </h1> + + <p className="text-xl text-gray-600 leading-relaxed"> + Create QR codes you can edit anytime - even after printing. Change URLs, track scans, and update content without reprinting. The smart choice for businesses. + </p> + + <div className="space-y-3"> + {[ + 'Edit content after printing', + 'Track scans and analytics', + 'A/B test without reprinting', + 'Custom branding and design', + ].map((feature, index) => ( + <div key={index} className="flex items-center space-x-3"> + <div className="flex-shrink-0 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"> + <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + </div> + <span className="text-gray-700">{feature}</span> + </div> + ))} + </div> + + <div className="flex flex-col sm:flex-row gap-4"> + <Link href="/signup"> + <Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> + Create Dynamic QR Code + </Button> + </Link> + <Link href="/pricing"> + <Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> + View Pricing + </Button> + </Link> + </div> + </div> + + {/* Visual Demo */} + <div className="relative"> + <Card className="p-8 shadow-2xl"> + <div className="text-center mb-6"> + <div className="inline-block bg-gray-200 rounded-lg p-8"> + <div className="w-48 h-48 bg-black rounded-lg flex items-center justify-center"> + <span className="text-white text-sm font-mono">QR Code</span> + </div> + </div> + </div> + <div className="space-y-3 text-sm"> + <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"> + <span className="text-gray-700">Current URL:</span> + <span className="text-blue-600 font-mono">summer-sale.com</span> + </div> + <div className="flex items-center justify-center"> + <svg className="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" /> + </svg> + </div> + <div className="flex items-center justify-between p-3 bg-green-50 rounded-lg"> + <span className="text-gray-700">Updated URL:</span> + <span className="text-green-600 font-mono">fall-sale.com</span> + </div> + </div> + <p className="text-center text-sm text-gray-600 mt-4"> + Same QR code, different destination! + </p> + </Card> + <div className="absolute -top-4 -right-4 bg-purple-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg"> + No Reprint Needed! + </div> + </div> + </div> + </div> + </section> + + {/* Static vs Dynamic */} + <section className="py-20 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + Dynamic vs Static QR Codes + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Understand why dynamic QR codes are the smart choice for businesses + </p> + </div> + + <Card className="overflow-hidden shadow-xl"> + <div className="grid md:grid-cols-3"> + <div className="p-6 bg-white"> + <h3 className="font-semibold text-lg mb-4">Feature</h3> + {staticVsDynamic.map((item, index) => ( + <div key={index} className="py-4 border-b last:border-b-0"> + <p className="text-gray-900 font-medium">{item.feature}</p> + </div> + ))} + </div> + <div className="p-6 bg-gray-50"> + <h3 className="font-semibold text-lg mb-4 text-gray-600">Static QR</h3> + {staticVsDynamic.map((item, index) => ( + <div key={index} className="py-4 border-b last:border-b-0 flex items-center justify-center"> + {item.static ? ( + <span className="text-green-500 text-2xl">✓</span> + ) : ( + <span className="text-red-500 text-2xl">✗</span> + )} + </div> + ))} + </div> + <div className="p-6 bg-primary-50"> + <h3 className="font-semibold text-lg mb-4 text-primary-600">Dynamic QR</h3> + {staticVsDynamic.map((item, index) => ( + <div key={index} className="py-4 border-b last:border-b-0 flex items-center justify-center"> + {item.dynamic ? ( + <span className="text-green-500 text-2xl">✓</span> + ) : ( + <span className="text-red-500 text-2xl">✗</span> + )} + </div> + ))} + </div> + </div> + </Card> + </div> + </section> + + {/* Features */} + <section className="py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + Powerful Dynamic QR Features + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Everything you need to create, manage, and optimize your QR code campaigns + </p> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> + {dynamicFeatures.map((feature, index) => ( + <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> + <div className="text-4xl mb-4">{feature.icon}</div> + <h3 className="text-xl font-semibold text-gray-900 mb-2"> + {feature.title} + </h3> + <p className="text-gray-600"> + {feature.description} + </p> + </Card> + ))} + </div> + </div> + </section> + + {/* Use Cases */} + <section className="py-20 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + How Businesses Use Dynamic QR Codes + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Real-world examples of dynamic QR code applications + </p> + </div> + + <div className="grid md:grid-cols-2 gap-8"> + {useCases.map((useCase, index) => ( + <Card key={index} className="p-8"> + <div className="flex items-start space-x-4"> + <div className="text-4xl">{useCase.icon}</div> + <div className="flex-1"> + <h3 className="text-2xl font-bold text-gray-900 mb-3"> + {useCase.title} + </h3> + <p className="text-gray-600 mb-4"> + {useCase.description} + </p> + <div className="bg-blue-50 border-l-4 border-blue-500 p-4"> + <p className="text-sm text-gray-700"> + <strong>Example:</strong> {useCase.example} + </p> + </div> + </div> + </div> + </Card> + ))} + </div> + </div> + </section> + + {/* How It Works */} + <section className="py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + How Dynamic QR Codes Work + </h2> + <p className="text-xl text-gray-600"> + Simple technology, powerful results + </p> + </div> + + <div className="grid md:grid-cols-3 gap-8"> + <Card className="p-6 text-center"> + <div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4"> + <span className="text-2xl font-bold text-primary-600">1</span> + </div> + <h3 className="text-xl font-semibold text-gray-900 mb-2">Create QR Code</h3> + <p className="text-gray-600"> + Generate a dynamic QR code with a short redirect URL + </p> + </Card> + + <Card className="p-6 text-center"> + <div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4"> + <span className="text-2xl font-bold text-primary-600">2</span> + </div> + <h3 className="text-xl font-semibold text-gray-900 mb-2">Print Anywhere</h3> + <p className="text-gray-600"> + Add to packaging, posters, cards, or anywhere you need + </p> + </Card> + + <Card className="p-6 text-center"> + <div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4"> + <span className="text-2xl font-bold text-primary-600">3</span> + </div> + <h3 className="text-xl font-semibold text-gray-900 mb-2">Update Anytime</h3> + <p className="text-gray-600"> + Change the destination URL from your dashboard whenever needed + </p> + </Card> + </div> + </div> + </section> + + {/* CTA Section */} + <section className="py-20 bg-gradient-to-r from-purple-600 to-blue-600 text-white"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> + <h2 className="text-4xl font-bold mb-6"> + Start Creating Dynamic QR Codes Today + </h2> + <p className="text-xl mb-8 text-purple-100"> + Join thousands of businesses who never worry about reprinting QR codes again + </p> + <div className="flex flex-col sm:flex-row gap-4 justify-center"> + <Link href="/signup"> + <Button size="lg" variant="secondary" className="text-lg px-8 py-4 w-full sm:w-auto bg-white text-purple-600 hover:bg-gray-100"> + Get Started Free + </Button> + </Link> + <Link href="/create"> + <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"> + Create QR Code Now + </Button> + </Link> + </div> + </div> + </section> + </div> + </> + ); +} diff --git a/src/app/(marketing)/faq/page.tsx b/src/app/(marketing)/faq/page.tsx index b69ac5d..860f78f 100644 --- a/src/app/(marketing)/faq/page.tsx +++ b/src/app/(marketing)/faq/page.tsx @@ -1,143 +1,143 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import { faqPageSchema } from '@/lib/schema'; -import { Card, CardContent } from '@/components/ui/Card'; - -function truncateAtWord(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - const truncated = text.slice(0, maxLength); - const lastSpace = truncated.lastIndexOf(' '); - return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; -} - -export async function generateMetadata(): Promise<Metadata> { - const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60); - const description = truncateAtWord( - 'All answers: dynamic QR, security, analytics, bulk, events & print.', - 160 - ); - - return { - title, - description, - alternates: { - canonical: 'https://www.qrmaster.net/faq', - languages: { - 'x-default': 'https://www.qrmaster.net/faq', - en: 'https://www.qrmaster.net/faq', - }, - }, - openGraph: { - title, - description, - url: 'https://www.qrmaster.net/faq', - type: 'website', - }, - twitter: { - title, - description, - }, - }; -} - -const faqs = [ - { - question: 'What is a dynamic QR code?', - answer: 'A dynamic QR code allows you to change the destination URL after the code has been created and printed. Unlike static QR codes, dynamic codes redirect through a short URL that you control, enabling real-time updates, scan analytics, and campaign tracking without reprinting the code.', - }, - { - question: 'How do I track QR scans?', - answer: 'QR Master provides a comprehensive analytics dashboard that tracks every scan in real-time. You can monitor scan rates, geographic locations, device types, timestamps, and user behavior. Enable UTM parameters to integrate with Google Analytics for advanced campaign tracking and conversion attribution.', - }, - { - question: 'What security features does QR Master offer?', - answer: 'QR Master employs enterprise-grade security including SSL encryption, link validation to prevent malicious redirects, fraud detection, and GDPR-compliant data handling. All scan analytics are stored securely and access is protected with multi-factor authentication for business accounts.', - }, - { - question: 'Can I generate bulk QR codes for print?', - answer: 'Yes. Our bulk QR generation tool allows you to create thousands of QR codes at once by uploading a CSV file. Each code can be customized with unique URLs, UTM parameters, and branding. Download print-ready files in SVG, PNG, or PDF formats optimized for high-resolution printing.', - }, - { - question: 'How do I brand my QR codes?', - answer: 'QR Master offers customization options including custom colors, corner styles, and pattern designs. Branded QR codes maintain scannability while matching your brand identity. Choose your color palette and preview designs before downloading.', - }, - { - question: 'Is scan analytics GDPR compliant?', - answer: 'Yes. All QR Master analytics are fully GDPR compliant. We collect only necessary data, provide transparent privacy policies, allow users to opt out, and store data securely in EU-compliant data centers. You maintain full control over data retention and deletion.', - }, - { - question: 'Can QR Master track campaigns with UTM?', - answer: 'Absolutely. QR Master supports UTM parameter integration for all dynamic QR codes. Automatically append source, medium, campaign, term, and content parameters to track QR performance in Google Analytics, Adobe Analytics, and other marketing platforms. UTM tracking enables multi-channel attribution and ROI measurement.', - }, - { - question: 'Difference between static and dynamic QR codes?', - answer: 'Static QR codes encode the destination URL directly in the code pattern and cannot be changed after creation. Dynamic QR codes use a short redirect URL, allowing you to update destinations, track scans, enable/disable codes, and gather analytics—all without reprinting. Dynamic codes are essential for professional marketing campaigns.', - }, - { - question: 'How are QR codes used for events?', - answer: 'QR codes streamline event check-ins, ticket validation, attendee tracking, and engagement measurement. Generate unique codes for each ticket, track scan times and locations, enable contactless entry, and analyze attendee behavior. Event organizers use QR analytics to measure session popularity and optimize future events.', - }, - { - question: 'Can I make QR codes for business cards?', - answer: 'Yes. QR codes on business cards provide instant contact sharing via vCard format, link to your portfolio or LinkedIn profile, and track networking effectiveness. Use branded QR codes that match your card design, and leverage scan analytics to see how many contacts engage and when they follow up.', - }, - { - question: 'How do I use QR codes for bulk marketing?', - answer: 'Bulk QR codes enable scalable campaigns across print ads, packaging, direct mail, and retail displays. Generate thousands of codes with unique tracking URLs, distribute them across channels, and use analytics to measure which placements drive the highest engagement. Bulk generation supports CSV upload, API integration, and automated workflows.', - }, - { - question: 'Is API access available for bulk QR generation?', - answer: 'Yes. QR Master offers a developer-friendly REST API for programmatic QR code generation, URL management, and analytics retrieval. Integrate QR creation into your CRM, marketing automation platform, or e-commerce system. API access is included in Business plans and supports bulk operations, webhooks, and real-time updates.', - }, -]; - -export default function FAQPage() { - return ( - <> - <SeoJsonLd data={faqPageSchema(faqs)} /> - <div className="py-20 bg-gradient-to-b from-gray-50 to-white"> - <div className="container mx-auto px-4"> - <div className="max-w-4xl mx-auto"> - <div className="text-center mb-16"> - <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> - Frequently Asked Questions - </h1> - <p className="text-xl text-gray-600"> - Everything you need to know about dynamic QR codes, security, analytics, bulk generation, events, and print quality. - </p> - </div> - - <div className="space-y-6"> - {faqs.map((faq, index) => ( - <Card key={index} className="border-l-4 border-blue-500"> - <CardContent className="p-8"> - <h2 className="text-2xl font-semibold mb-4 text-gray-900"> - {faq.question} - </h2> - <p className="text-lg text-gray-700 leading-relaxed"> - {faq.answer} - </p> - </CardContent> - </Card> - ))} - </div> - - <div className="mt-16 bg-blue-50 border-l-4 border-blue-500 p-8 rounded-r-lg"> - <h2 className="text-2xl font-bold mb-4 text-gray-900"> - Still have questions? - </h2> - <p className="text-lg text-gray-700 mb-6 leading-relaxed"> - Our support team is here to help. Contact us at{' '} - <a href="mailto:support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold"> - support@qrmaster.net - </a>{' '} - or reach out through our live chat. - </p> - </div> - </div> - </div> - </div> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import { faqPageSchema } from '@/lib/schema'; +import { Card, CardContent } from '@/components/ui/Card'; + +function truncateAtWord(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + const truncated = text.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(' '); + return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; +} + +export async function generateMetadata(): Promise<Metadata> { + const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60); + const description = truncateAtWord( + 'All answers: dynamic QR, security, analytics, bulk, events & print.', + 160 + ); + + return { + title, + description, + alternates: { + canonical: 'https://www.qrmaster.net/faq', + languages: { + 'x-default': 'https://www.qrmaster.net/faq', + en: 'https://www.qrmaster.net/faq', + }, + }, + openGraph: { + title, + description, + url: 'https://www.qrmaster.net/faq', + type: 'website', + }, + twitter: { + title, + description, + }, + }; +} + +const faqs = [ + { + question: 'What is a dynamic QR code?', + answer: 'A dynamic QR code allows you to change the destination URL after the code has been created and printed. Unlike static QR codes, dynamic codes redirect through a short URL that you control, enabling real-time updates, scan analytics, and campaign tracking without reprinting the code.', + }, + { + question: 'How do I track QR scans?', + answer: 'QR Master provides a comprehensive analytics dashboard that tracks every scan in real-time. You can monitor scan rates, geographic locations, device types, timestamps, and user behavior. Enable UTM parameters to integrate with Google Analytics for advanced campaign tracking and conversion attribution.', + }, + { + question: 'What security features does QR Master offer?', + answer: 'QR Master employs enterprise-grade security including SSL encryption, link validation to prevent malicious redirects, fraud detection, and GDPR-compliant data handling. All scan analytics are stored securely and access is protected with multi-factor authentication for business accounts.', + }, + { + question: 'Can I generate bulk QR codes for print?', + answer: 'Yes. Our bulk QR generation tool allows you to create thousands of QR codes at once by uploading a CSV file. Each code can be customized with unique URLs, UTM parameters, and branding. Download print-ready files in SVG, PNG, or PDF formats optimized for high-resolution printing.', + }, + { + question: 'How do I brand my QR codes?', + answer: 'QR Master offers customization options including custom colors, corner styles, and pattern designs. Branded QR codes maintain scannability while matching your brand identity. Choose your color palette and preview designs before downloading.', + }, + { + question: 'Is scan analytics GDPR compliant?', + answer: 'Yes. All QR Master analytics are fully GDPR compliant. We collect only necessary data, provide transparent privacy policies, allow users to opt out, and store data securely in EU-compliant data centers. You maintain full control over data retention and deletion.', + }, + { + question: 'Can QR Master track campaigns with UTM?', + answer: 'Absolutely. QR Master supports UTM parameter integration for all dynamic QR codes. Automatically append source, medium, campaign, term, and content parameters to track QR performance in Google Analytics, Adobe Analytics, and other marketing platforms. UTM tracking enables multi-channel attribution and ROI measurement.', + }, + { + question: 'Difference between static and dynamic QR codes?', + answer: 'Static QR codes encode the destination URL directly in the code pattern and cannot be changed after creation. Dynamic QR codes use a short redirect URL, allowing you to update destinations, track scans, enable/disable codes, and gather analytics—all without reprinting. Dynamic codes are essential for professional marketing campaigns.', + }, + { + question: 'How are QR codes used for events?', + answer: 'QR codes streamline event check-ins, ticket validation, attendee tracking, and engagement measurement. Generate unique codes for each ticket, track scan times and locations, enable contactless entry, and analyze attendee behavior. Event organizers use QR analytics to measure session popularity and optimize future events.', + }, + { + question: 'Can I make QR codes for business cards?', + answer: 'Yes. QR codes on business cards provide instant contact sharing via vCard format, link to your portfolio or LinkedIn profile, and track networking effectiveness. Use branded QR codes that match your card design, and leverage scan analytics to see how many contacts engage and when they follow up.', + }, + { + question: 'How do I use QR codes for bulk marketing?', + answer: 'Bulk QR codes enable scalable campaigns across print ads, packaging, direct mail, and retail displays. Generate thousands of codes with unique tracking URLs, distribute them across channels, and use analytics to measure which placements drive the highest engagement. Bulk generation supports CSV upload, API integration, and automated workflows.', + }, + { + question: 'Is API access available for bulk QR generation?', + answer: 'Yes. QR Master offers a developer-friendly REST API for programmatic QR code generation, URL management, and analytics retrieval. Integrate QR creation into your CRM, marketing automation platform, or e-commerce system. API access is included in Business plans and supports bulk operations, webhooks, and real-time updates.', + }, +]; + +export default function FAQPage() { + return ( + <> + <SeoJsonLd data={faqPageSchema(faqs)} /> + <div className="py-20 bg-gradient-to-b from-gray-50 to-white"> + <div className="container mx-auto px-4"> + <div className="max-w-4xl mx-auto"> + <div className="text-center mb-16"> + <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6"> + Frequently Asked Questions + </h1> + <p className="text-xl text-gray-600"> + Everything you need to know about dynamic QR codes, security, analytics, bulk generation, events, and print quality. + </p> + </div> + + <div className="space-y-6"> + {faqs.map((faq, index) => ( + <Card key={index} className="border-l-4 border-blue-500"> + <CardContent className="p-8"> + <h2 className="text-2xl font-semibold mb-4 text-gray-900"> + {faq.question} + </h2> + <p className="text-lg text-gray-700 leading-relaxed"> + {faq.answer} + </p> + </CardContent> + </Card> + ))} + </div> + + <div className="mt-16 bg-blue-50 border-l-4 border-blue-500 p-8 rounded-r-lg"> + <h2 className="text-2xl font-bold mb-4 text-gray-900"> + Still have questions? + </h2> + <p className="text-lg text-gray-700 mb-6 leading-relaxed"> + Our support team is here to help. Contact us at{' '} + <a href="mailto:support@qrmaster.net" className="text-blue-600 hover:text-blue-700 font-semibold"> + support@qrmaster.net + </a>{' '} + or reach out through our live chat. + </p> + </div> + </div> + </div> + </div> + </> + ); +} diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index 6623c40..5d5650a 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -1,71 +1,71 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import { organizationSchema, websiteSchema } from '@/lib/schema'; -import HomePageClient from '@/components/marketing/HomePageClient'; - -function truncateAtWord(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - const truncated = text.slice(0, maxLength); - const lastSpace = truncated.lastIndexOf(' '); - return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; -} - -export async function generateMetadata(): Promise<Metadata> { - const title = truncateAtWord('QR Master: Dynamic QR Generator', 60); - const description = truncateAtWord( - 'Dynamic QR, branding, bulk generation & analytics for all campaigns.', - 160 - ); - - return { - title, - description, - alternates: { - canonical: 'https://www.qrmaster.net/', - languages: { - 'x-default': 'https://www.qrmaster.net/', - en: 'https://www.qrmaster.net/', - }, - }, - openGraph: { - title, - description, - url: 'https://www.qrmaster.net/', - type: 'website', - }, - twitter: { - title, - description, - }, - }; -} - -export default function HomePage() { - return ( - <> - <SeoJsonLd data={[organizationSchema(), websiteSchema()]} /> - - {/* Server-rendered SEO content for crawlers */} - <div className="sr-only" aria-hidden="false"> - <h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1> - <p> - Create professional QR codes for your business with QR Master. Our dynamic QR code generator - lets you create trackable QR codes, edit destinations anytime, and view detailed analytics. - Perfect for restaurants, retail, events, and marketing campaigns. - </p> - <p> - Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV, - custom branding with colors and logos, advanced scan analytics showing device types and locations, - vCard QR codes for digital business cards, and restaurant menu QR codes. - </p> - <p> - Start free with 3 dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes - with advanced analytics, or Business for 500 codes with bulk creation and priority support. - </p> - </div> - - <HomePageClient /> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import { organizationSchema, websiteSchema } from '@/lib/schema'; +import HomePageClient from '@/components/marketing/HomePageClient'; + +function truncateAtWord(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + const truncated = text.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(' '); + return lastSpace > 0 ? truncated.slice(0, lastSpace) : truncated; +} + +export async function generateMetadata(): Promise<Metadata> { + const title = truncateAtWord('QR Master: Dynamic QR Generator', 60); + const description = truncateAtWord( + 'Dynamic QR, branding, bulk generation & analytics for all campaigns.', + 160 + ); + + return { + title, + description, + alternates: { + canonical: 'https://www.qrmaster.net/', + languages: { + 'x-default': 'https://www.qrmaster.net/', + en: 'https://www.qrmaster.net/', + }, + }, + openGraph: { + title, + description, + url: 'https://www.qrmaster.net/', + type: 'website', + }, + twitter: { + title, + description, + }, + }; +} + +export default function HomePage() { + return ( + <> + <SeoJsonLd data={[organizationSchema(), websiteSchema()]} /> + + {/* Server-rendered SEO content for crawlers */} + <div className="sr-only" aria-hidden="false"> + <h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1> + <p> + Create professional QR codes for your business with QR Master. Our dynamic QR code generator + lets you create trackable QR codes, edit destinations anytime, and view detailed analytics. + Perfect for restaurants, retail, events, and marketing campaigns. + </p> + <p> + Features include: Dynamic QR codes with real-time tracking, bulk QR code generation from Excel/CSV, + custom branding with colors and logos, advanced scan analytics showing device types and locations, + vCard QR codes for digital business cards, and restaurant menu QR codes. + </p> + <p> + Start free with 3 dynamic QR codes and unlimited static codes. Upgrade to Pro for 50 codes + with advanced analytics, or Business for 500 codes with bulk creation and priority support. + </p> + </div> + + <HomePageClient /> + </> + ); +} diff --git a/src/app/(marketing)/privacy/page.tsx b/src/app/(marketing)/privacy/page.tsx index afe1ba6..4af8a16 100644 --- a/src/app/(marketing)/privacy/page.tsx +++ b/src/app/(marketing)/privacy/page.tsx @@ -1,133 +1,133 @@ -import React from 'react'; -import Link from 'next/link'; - -export const metadata = { - title: 'Privacy Policy | QR Master', - description: 'Privacy Policy and data protection information for QR Master', -}; - -export default function PrivacyPage() { - return ( - <div className="min-h-screen bg-white py-12"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl"> - <div className="mb-8"> - <Link href="/" className="text-primary-600 hover:text-primary-700 font-medium"> - ← Back to Home - </Link> - </div> - - <h1 className="text-4xl font-bold text-gray-900 mb-4">Privacy Policy</h1> - <p className="text-gray-600 mb-8">Last updated: January 2025</p> - - <div className="prose prose-lg max-w-none"> - <section className="mb-8"> - <h2 className="text-2xl font-bold text-gray-900 mb-4">1. Introduction</h2> - <p className="text-gray-700 mb-4"> - Welcome to QR Master ("we," "our," or "us"). We respect your privacy and are committed to protecting your personal data. - This privacy policy explains how we collect, use, and protect your information when you use our services. - </p> - <p className="text-gray-700 mb-4"> - We implement appropriate security measures including secure HTTPS transmission, password hashing, database access controls, - and CSRF protection to keep your data safe. - </p> - </section> - - <section className="mb-8"> - <h2 className="text-2xl font-bold text-gray-900 mb-4">2. Information We Collect</h2> - - <h3 className="text-xl font-semibold text-gray-900 mb-3">Information You Provide</h3> - <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> - <li><strong>Account Information:</strong> Name, email address, and password</li> - <li><strong>Payment Information:</strong> Processed securely through Stripe (we do not store credit card information)</li> - <li><strong>QR Code Content:</strong> URLs, text, and customization settings for your QR codes</li> - </ul> - - <h3 className="text-xl font-semibold text-gray-900 mb-3">Information Collected Automatically</h3> - <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> - <li><strong>Usage Data:</strong> QR code scans and analytics</li> - <li><strong>Technical Data:</strong> IP address, browser type, and device information</li> - <li><strong>Cookies:</strong> Essential cookies for authentication and optional analytics cookies (PostHog) with your consent</li> - </ul> - </section> - - <section className="mb-8"> - <h2 className="text-2xl font-bold text-gray-900 mb-4">3. How We Use Your Information</h2> - <p className="text-gray-700 mb-4">We use your data to:</p> - <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> - <li>Provide and maintain our QR code services</li> - <li>Process payments and manage subscriptions</li> - <li>Provide customer support</li> - <li>Improve our services and develop new features</li> - <li>Detect and prevent fraud</li> - </ul> - <p className="text-gray-700 mb-4"> - We retain your data while your account is active. Upon account deletion, most data is removed immediately, - though some may be retained for legal compliance. Aggregated, anonymized analytics may be kept indefinitely. - </p> - </section> - - <section className="mb-8"> - <h2 className="text-2xl font-bold text-gray-900 mb-4">4. Data Sharing</h2> - <p className="text-gray-700 mb-4">We may share your data with:</p> - <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> - <li><strong>Stripe:</strong> Payment processing</li> - <li><strong>PostHog:</strong> Analytics (only with your consent, respects Do Not Track)</li> - <li><strong>Vercel:</strong> Cloud hosting provider</li> - <li><strong>Legal Requirements:</strong> When required by law</li> - </ul> - <p className="text-gray-700 mb-4"> - We do not sell your personal data. Analytics are only activated if you accept optional cookies. - </p> - </section> - - <section className="mb-8"> - <h2 className="text-2xl font-bold text-gray-900 mb-4">5. Your Rights (GDPR)</h2> - <p className="text-gray-700 mb-4">You have the right to:</p> - <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> - <li><strong>Access:</strong> Request a copy of your personal data</li> - <li><strong>Rectification:</strong> Correct inaccurate data (update in account settings)</li> - <li><strong>Erasure:</strong> Delete your data (account deletion available in settings)</li> - <li><strong>Data Portability:</strong> Receive your data in a portable format</li> - <li><strong>Object:</strong> Object to processing based on legitimate interests</li> - <li><strong>Withdraw Consent:</strong> Withdraw cookie consent at any time</li> - </ul> - <p className="text-gray-700 mb-4"> - To exercise these rights, contact us at{' '} - <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700"> - support@qrmaster.net - </a> - </p> - <p className="text-gray-700 mb-4"> - Our service is for users 16 years and older. If you're in the EEA and have concerns, - you may lodge a complaint with your local data protection authority. - </p> - </section> - - <section className="mb-8"> - <h2 className="text-2xl font-bold text-gray-900 mb-4">6. Contact Us</h2> - <p className="text-gray-700 mb-4"> - If you have questions about this privacy policy, please contact us: - </p> - <div className="bg-gray-50 p-6 rounded-lg"> - <p className="text-gray-700 mb-2"> - <strong>Email:</strong>{' '} - <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700"> - support@qrmaster.net - </a> - </p> - <p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p> - </div> - </section> - </div> - - <div className="mt-12 pt-8 border-t border-gray-200"> - <p className="text-gray-600 text-center"> - <Link href="/" className="text-primary-600 hover:text-primary-700"> - Back to Home - </Link> - </p> - </div> - </div> - </div> - ); -} +import React from 'react'; +import Link from 'next/link'; + +export const metadata = { + title: 'Privacy Policy | QR Master', + description: 'Privacy Policy and data protection information for QR Master', +}; + +export default function PrivacyPage() { + return ( + <div className="min-h-screen bg-white py-12"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl"> + <div className="mb-8"> + <Link href="/" className="text-primary-600 hover:text-primary-700 font-medium"> + ← Back to Home + </Link> + </div> + + <h1 className="text-4xl font-bold text-gray-900 mb-4">Privacy Policy</h1> + <p className="text-gray-600 mb-8">Last updated: January 2025</p> + + <div className="prose prose-lg max-w-none"> + <section className="mb-8"> + <h2 className="text-2xl font-bold text-gray-900 mb-4">1. Introduction</h2> + <p className="text-gray-700 mb-4"> + Welcome to QR Master ("we," "our," or "us"). We respect your privacy and are committed to protecting your personal data. + This privacy policy explains how we collect, use, and protect your information when you use our services. + </p> + <p className="text-gray-700 mb-4"> + We implement appropriate security measures including secure HTTPS transmission, password hashing, database access controls, + and CSRF protection to keep your data safe. + </p> + </section> + + <section className="mb-8"> + <h2 className="text-2xl font-bold text-gray-900 mb-4">2. Information We Collect</h2> + + <h3 className="text-xl font-semibold text-gray-900 mb-3">Information You Provide</h3> + <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> + <li><strong>Account Information:</strong> Name, email address, and password</li> + <li><strong>Payment Information:</strong> Processed securely through Stripe (we do not store credit card information)</li> + <li><strong>QR Code Content:</strong> URLs, text, and customization settings for your QR codes</li> + </ul> + + <h3 className="text-xl font-semibold text-gray-900 mb-3">Information Collected Automatically</h3> + <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> + <li><strong>Usage Data:</strong> QR code scans and analytics</li> + <li><strong>Technical Data:</strong> IP address, browser type, and device information</li> + <li><strong>Cookies:</strong> Essential cookies for authentication and optional analytics cookies (PostHog) with your consent</li> + </ul> + </section> + + <section className="mb-8"> + <h2 className="text-2xl font-bold text-gray-900 mb-4">3. How We Use Your Information</h2> + <p className="text-gray-700 mb-4">We use your data to:</p> + <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> + <li>Provide and maintain our QR code services</li> + <li>Process payments and manage subscriptions</li> + <li>Provide customer support</li> + <li>Improve our services and develop new features</li> + <li>Detect and prevent fraud</li> + </ul> + <p className="text-gray-700 mb-4"> + We retain your data while your account is active. Upon account deletion, most data is removed immediately, + though some may be retained for legal compliance. Aggregated, anonymized analytics may be kept indefinitely. + </p> + </section> + + <section className="mb-8"> + <h2 className="text-2xl font-bold text-gray-900 mb-4">4. Data Sharing</h2> + <p className="text-gray-700 mb-4">We may share your data with:</p> + <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> + <li><strong>Stripe:</strong> Payment processing</li> + <li><strong>PostHog:</strong> Analytics (only with your consent, respects Do Not Track)</li> + <li><strong>Vercel:</strong> Cloud hosting provider</li> + <li><strong>Legal Requirements:</strong> When required by law</li> + </ul> + <p className="text-gray-700 mb-4"> + We do not sell your personal data. Analytics are only activated if you accept optional cookies. + </p> + </section> + + <section className="mb-8"> + <h2 className="text-2xl font-bold text-gray-900 mb-4">5. Your Rights (GDPR)</h2> + <p className="text-gray-700 mb-4">You have the right to:</p> + <ul className="list-disc pl-6 mb-4 text-gray-700 space-y-2"> + <li><strong>Access:</strong> Request a copy of your personal data</li> + <li><strong>Rectification:</strong> Correct inaccurate data (update in account settings)</li> + <li><strong>Erasure:</strong> Delete your data (account deletion available in settings)</li> + <li><strong>Data Portability:</strong> Receive your data in a portable format</li> + <li><strong>Object:</strong> Object to processing based on legitimate interests</li> + <li><strong>Withdraw Consent:</strong> Withdraw cookie consent at any time</li> + </ul> + <p className="text-gray-700 mb-4"> + To exercise these rights, contact us at{' '} + <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700"> + support@qrmaster.net + </a> + </p> + <p className="text-gray-700 mb-4"> + Our service is for users 16 years and older. If you're in the EEA and have concerns, + you may lodge a complaint with your local data protection authority. + </p> + </section> + + <section className="mb-8"> + <h2 className="text-2xl font-bold text-gray-900 mb-4">6. Contact Us</h2> + <p className="text-gray-700 mb-4"> + If you have questions about this privacy policy, please contact us: + </p> + <div className="bg-gray-50 p-6 rounded-lg"> + <p className="text-gray-700 mb-2"> + <strong>Email:</strong>{' '} + <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700"> + support@qrmaster.net + </a> + </p> + <p className="text-gray-700 mb-2"><strong>Website:</strong> <a href="/" className="text-primary-600 hover:text-primary-700">qrmaster.net</a></p> + </div> + </section> + </div> + + <div className="mt-12 pt-8 border-t border-gray-200"> + <p className="text-gray-600 text-center"> + <Link href="/" className="text-primary-600 hover:text-primary-700"> + Back to Home + </Link> + </p> + </div> + </div> + </div> + ); +} diff --git a/src/app/(marketing)/qr-code-tracking/page.tsx b/src/app/(marketing)/qr-code-tracking/page.tsx index 32e2717..4e267be 100644 --- a/src/app/(marketing)/qr-code-tracking/page.tsx +++ b/src/app/(marketing)/qr-code-tracking/page.tsx @@ -1,398 +1,398 @@ -import React from 'react'; -import type { Metadata } from 'next'; -import Link from 'next/link'; -import { Button } from '@/components/ui/Button'; -import { Card } from '@/components/ui/Card'; -import SeoJsonLd from '@/components/SeoJsonLd'; -import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; -import { breadcrumbSchema } from '@/lib/schema'; - -export const metadata: Metadata = { - title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', - description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.', - keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring', - alternates: { - canonical: 'https://www.qrmaster.net/qr-code-tracking', - languages: { - 'x-default': 'https://www.qrmaster.net/qr-code-tracking', - en: 'https://www.qrmaster.net/qr-code-tracking', - }, - }, - openGraph: { - title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', - description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.', - url: 'https://www.qrmaster.net/qr-code-tracking', - type: 'website', - }, - twitter: { - title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', - description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.', - }, -}; - -export default function QRCodeTrackingPage() { - const trackingFeatures = [ - { - icon: '📊', - title: 'Real-Time Analytics', - description: 'See scan data instantly as it happens. Monitor your QR code performance in real-time with live dashboards.', - }, - { - icon: '🌍', - title: 'Location Tracking', - description: 'Know exactly where your QR codes are being scanned. Track by country, city, and region.', - }, - { - icon: 'đŸ“±', - title: 'Device Detection', - description: 'Identify which devices scan your codes. Track iOS, Android, desktop, and browser types.', - }, - { - icon: '🕐', - title: 'Time-Based Reports', - description: 'Analyze scan patterns by hour, day, week, or month. Optimize your campaigns with timing insights.', - }, - { - icon: 'đŸ‘„', - title: 'Unique vs Total Scans', - description: 'Distinguish between unique users and repeat scans. Measure true reach and engagement.', - }, - { - icon: '📈', - title: 'Campaign Performance', - description: 'Track ROI with UTM parameters. Measure conversion rates and campaign effectiveness.', - }, - ]; - - const useCases = [ - { - title: 'Marketing Campaigns', - description: 'Track print ads, billboards, and product packaging to measure marketing ROI.', - benefits: ['Measure ad performance', 'A/B test campaigns', 'Track conversions'], - }, - { - title: 'Event Management', - description: 'Monitor event check-ins, booth visits, and attendee engagement in real-time.', - benefits: ['Live attendance tracking', 'Booth analytics', 'Engagement metrics'], - }, - { - title: 'Product Labels', - description: 'Track product authenticity scans, manual downloads, and warranty registrations.', - benefits: ['Anti-counterfeiting', 'User registration tracking', 'Product analytics'], - }, - { - title: 'Restaurant Menus', - description: 'See how many customers scan your menu QR codes and when peak times occur.', - benefits: ['Customer insights', 'Peak time analysis', 'Menu engagement'], - }, - ]; - - const comparisonData = [ - { feature: 'Real-Time Analytics', free: true, qrMaster: true }, - { feature: 'Location Tracking', free: false, qrMaster: true }, - { feature: 'Device Detection', free: false, qrMaster: true }, - { feature: 'Unlimited Scans', free: false, qrMaster: true }, - { feature: 'Historical Data', free: '7 days', qrMaster: 'Unlimited' }, - { feature: 'Export Reports', free: false, qrMaster: true }, - { feature: 'API Access', free: false, qrMaster: true }, - ]; - - const softwareSchema = { - '@context': 'https://schema.org', - '@type': 'SoftwareApplication', - '@id': 'https://www.qrmaster.net/qr-code-tracking#software', - name: 'QR Master - QR Code Tracking & Analytics', - applicationCategory: 'BusinessApplication', - operatingSystem: 'Web Browser, iOS, Android', - offers: { - '@type': 'Offer', - price: '0', - priceCurrency: 'USD', - availability: 'https://schema.org/InStock', - }, - aggregateRating: { - '@type': 'AggregateRating', - ratingValue: '4.8', - ratingCount: '1250', - }, - description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior with our free QR code tracking software.', - features: [ - 'Real-time analytics dashboard', - 'Location tracking by country and city', - 'Device detection (iOS, Android, Desktop)', - 'Time-based scan reports', - 'Unique vs total scan tracking', - 'Campaign performance metrics', - 'Unlimited scans', - 'Export detailed reports', - ], - }; - - const howToSchema = { - '@context': 'https://schema.org', - '@type': 'HowTo', - '@id': 'https://www.qrmaster.net/qr-code-tracking#howto', - name: 'How to Track QR Code Scans', - description: 'Learn how to track and analyze QR code scans with real-time analytics', - totalTime: 'PT5M', - step: [ - { - '@type': 'HowToStep', - position: 1, - name: 'Create QR Code', - text: 'Sign up for free and create a dynamic QR code with tracking enabled', - url: 'https://www.qrmaster.net/signup', - }, - { - '@type': 'HowToStep', - position: 2, - name: 'Deploy QR Code', - text: 'Download and place your QR code on marketing materials, products, or digital platforms', - }, - { - '@type': 'HowToStep', - position: 3, - name: 'Monitor Analytics', - text: 'View real-time scan data including location, device, and time patterns in your dashboard', - url: 'https://www.qrmaster.net/analytics', - }, - { - '@type': 'HowToStep', - position: 4, - name: 'Optimize Campaigns', - text: 'Use insights to optimize placement, timing, and targeting of your QR code campaigns', - }, - ], - }; - - const breadcrumbItems: BreadcrumbItem[] = [ - { name: 'Home', url: '/' }, - { name: 'QR Code Tracking', url: '/qr-code-tracking' }, - ]; - - return ( - <> - <SeoJsonLd data={[softwareSchema, howToSchema, breadcrumbSchema(breadcrumbItems)]} /> - <div className="min-h-screen bg-white"> - {/* Hero Section */} - <section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <Breadcrumbs items={breadcrumbItems} /> - <div className="grid lg:grid-cols-2 gap-12 items-center"> - <div className="space-y-8"> - <div className="inline-flex items-center space-x-2 bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-semibold"> - <span>📊</span> - <span>Free QR Code Tracking</span> - </div> - - <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> - Track Every QR Code Scan with Powerful Analytics - </h1> - - <p className="text-xl text-gray-600 leading-relaxed"> - Monitor your QR code performance in real-time. Get detailed insights on location, device, time, and user behavior. Make data-driven decisions with our free tracking software. - </p> - - <div className="flex flex-col sm:flex-row gap-4"> - <Link href="/signup"> - <Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> - Start Tracking Free - </Button> - </Link> - <Link href="/create"> - <Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> - Create Trackable QR Code - </Button> - </Link> - </div> - - <div className="flex items-center space-x-6 text-sm text-gray-600"> - <div className="flex items-center space-x-2"> - <svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - <span>No credit card required</span> - </div> - <div className="flex items-center space-x-2"> - <svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - <span>Unlimited scans</span> - </div> - </div> - </div> - - {/* Analytics Preview */} - <div className="relative"> - <Card className="p-6 shadow-2xl"> - <h3 className="font-semibold text-lg mb-4">Live Analytics Dashboard</h3> - <div className="space-y-4"> - <div className="flex justify-between items-center pb-3 border-b"> - <span className="text-gray-600">Total Scans</span> - <span className="text-2xl font-bold text-primary-600">12,547</span> - </div> - <div className="flex justify-between items-center pb-3 border-b"> - <span className="text-gray-600">Unique Users</span> - <span className="text-2xl font-bold text-primary-600">8,392</span> - </div> - <div className="flex justify-between items-center pb-3 border-b"> - <span className="text-gray-600">Top Location</span> - <span className="font-semibold">đŸ‡©đŸ‡Ș Germany</span> - </div> - <div className="flex justify-between items-center"> - <span className="text-gray-600">Top Device</span> - <span className="font-semibold">đŸ“± iPhone</span> - </div> - </div> - </Card> - <div className="absolute -top-4 -right-4 bg-green-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg animate-pulse"> - Live Updates - </div> - </div> - </div> - </div> - </section> - - {/* Tracking Features */} - <section className="py-20 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - Powerful QR Code Tracking Features - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - Get complete visibility into your QR code performance with our comprehensive analytics suite - </p> - </div> - - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> - {trackingFeatures.map((feature, index) => ( - <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> - <div className="text-4xl mb-4">{feature.icon}</div> - <h3 className="text-xl font-semibold text-gray-900 mb-2"> - {feature.title} - </h3> - <p className="text-gray-600"> - {feature.description} - </p> - </Card> - ))} - </div> - </div> - </section> - - {/* Use Cases */} - <section className="py-20"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - QR Code Tracking Use Cases - </h2> - <p className="text-xl text-gray-600 max-w-3xl mx-auto"> - See how businesses use QR code tracking to improve their operations - </p> - </div> - - <div className="grid md:grid-cols-2 gap-8"> - {useCases.map((useCase, index) => ( - <Card key={index} className="p-8"> - <h3 className="text-2xl font-bold text-gray-900 mb-3"> - {useCase.title} - </h3> - <p className="text-gray-600 mb-6"> - {useCase.description} - </p> - <ul className="space-y-2"> - {useCase.benefits.map((benefit, idx) => ( - <li key={idx} className="flex items-center space-x-2"> - <svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - <span className="text-gray-700">{benefit}</span> - </li> - ))} - </ul> - </Card> - ))} - </div> - </div> - </section> - - {/* Comparison Table */} - <section className="py-20 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> - <div className="text-center mb-16"> - <h2 className="text-4xl font-bold text-gray-900 mb-4"> - QR Master vs Free Tools - </h2> - <p className="text-xl text-gray-600"> - See why businesses choose QR Master for QR code tracking - </p> - </div> - - <Card className="overflow-hidden"> - <table className="w-full"> - <thead className="bg-gray-100"> - <tr> - <th className="px-6 py-4 text-left text-gray-900 font-semibold">Feature</th> - <th className="px-6 py-4 text-center text-gray-900 font-semibold">Free Tools</th> - <th className="px-6 py-4 text-center text-primary-600 font-semibold">QR Master</th> - </tr> - </thead> - <tbody className="divide-y divide-gray-200"> - {comparisonData.map((row, index) => ( - <tr key={index}> - <td className="px-6 py-4 text-gray-900 font-medium">{row.feature}</td> - <td className="px-6 py-4 text-center"> - {typeof row.free === 'boolean' ? ( - row.free ? ( - <span className="text-green-500 text-2xl">✓</span> - ) : ( - <span className="text-red-500 text-2xl">✗</span> - ) - ) : ( - <span className="text-gray-600">{row.free}</span> - )} - </td> - <td className="px-6 py-4 text-center"> - {typeof row.qrMaster === 'boolean' ? ( - <span className="text-green-500 text-2xl">✓</span> - ) : ( - <span className="text-primary-600 font-semibold">{row.qrMaster}</span> - )} - </td> - </tr> - ))} - </tbody> - </table> - </Card> - </div> - </section> - - {/* CTA Section */} - <section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> - <h2 className="text-4xl font-bold mb-6"> - Start Tracking Your QR Codes Today - </h2> - <p className="text-xl mb-8 text-primary-100"> - Join thousands of businesses using QR Master to track and optimize their QR code campaigns - </p> - <div className="flex flex-col sm:flex-row gap-4 justify-center"> - <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"> - Create Free Account - </Button> - </Link> - <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"> - View Pricing - </Button> - </Link> - </div> - </div> - </section> - </div> - </> - ); -} +import React from 'react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; +import { Card } from '@/components/ui/Card'; +import SeoJsonLd from '@/components/SeoJsonLd'; +import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs'; +import { breadcrumbSchema } from '@/lib/schema'; + +export const metadata: Metadata = { + title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', + description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.', + keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring', + alternates: { + canonical: 'https://www.qrmaster.net/qr-code-tracking', + languages: { + 'x-default': 'https://www.qrmaster.net/qr-code-tracking', + en: 'https://www.qrmaster.net/qr-code-tracking', + }, + }, + openGraph: { + title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', + description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.', + url: 'https://www.qrmaster.net/qr-code-tracking', + type: 'website', + }, + twitter: { + title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', + description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.', + }, +}; + +export default function QRCodeTrackingPage() { + const trackingFeatures = [ + { + icon: '📊', + title: 'Real-Time Analytics', + description: 'See scan data instantly as it happens. Monitor your QR code performance in real-time with live dashboards.', + }, + { + icon: '🌍', + title: 'Location Tracking', + description: 'Know exactly where your QR codes are being scanned. Track by country, city, and region.', + }, + { + icon: 'đŸ“±', + title: 'Device Detection', + description: 'Identify which devices scan your codes. Track iOS, Android, desktop, and browser types.', + }, + { + icon: '🕐', + title: 'Time-Based Reports', + description: 'Analyze scan patterns by hour, day, week, or month. Optimize your campaigns with timing insights.', + }, + { + icon: 'đŸ‘„', + title: 'Unique vs Total Scans', + description: 'Distinguish between unique users and repeat scans. Measure true reach and engagement.', + }, + { + icon: '📈', + title: 'Campaign Performance', + description: 'Track ROI with UTM parameters. Measure conversion rates and campaign effectiveness.', + }, + ]; + + const useCases = [ + { + title: 'Marketing Campaigns', + description: 'Track print ads, billboards, and product packaging to measure marketing ROI.', + benefits: ['Measure ad performance', 'A/B test campaigns', 'Track conversions'], + }, + { + title: 'Event Management', + description: 'Monitor event check-ins, booth visits, and attendee engagement in real-time.', + benefits: ['Live attendance tracking', 'Booth analytics', 'Engagement metrics'], + }, + { + title: 'Product Labels', + description: 'Track product authenticity scans, manual downloads, and warranty registrations.', + benefits: ['Anti-counterfeiting', 'User registration tracking', 'Product analytics'], + }, + { + title: 'Restaurant Menus', + description: 'See how many customers scan your menu QR codes and when peak times occur.', + benefits: ['Customer insights', 'Peak time analysis', 'Menu engagement'], + }, + ]; + + const comparisonData = [ + { feature: 'Real-Time Analytics', free: true, qrMaster: true }, + { feature: 'Location Tracking', free: false, qrMaster: true }, + { feature: 'Device Detection', free: false, qrMaster: true }, + { feature: 'Unlimited Scans', free: false, qrMaster: true }, + { feature: 'Historical Data', free: '7 days', qrMaster: 'Unlimited' }, + { feature: 'Export Reports', free: false, qrMaster: true }, + { feature: 'API Access', free: false, qrMaster: true }, + ]; + + const softwareSchema = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + '@id': 'https://www.qrmaster.net/qr-code-tracking#software', + name: 'QR Master - QR Code Tracking & Analytics', + applicationCategory: 'BusinessApplication', + operatingSystem: 'Web Browser, iOS, Android', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + availability: 'https://schema.org/InStock', + }, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.8', + ratingCount: '1250', + }, + description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior with our free QR code tracking software.', + features: [ + 'Real-time analytics dashboard', + 'Location tracking by country and city', + 'Device detection (iOS, Android, Desktop)', + 'Time-based scan reports', + 'Unique vs total scan tracking', + 'Campaign performance metrics', + 'Unlimited scans', + 'Export detailed reports', + ], + }; + + const howToSchema = { + '@context': 'https://schema.org', + '@type': 'HowTo', + '@id': 'https://www.qrmaster.net/qr-code-tracking#howto', + name: 'How to Track QR Code Scans', + description: 'Learn how to track and analyze QR code scans with real-time analytics', + totalTime: 'PT5M', + step: [ + { + '@type': 'HowToStep', + position: 1, + name: 'Create QR Code', + text: 'Sign up for free and create a dynamic QR code with tracking enabled', + url: 'https://www.qrmaster.net/signup', + }, + { + '@type': 'HowToStep', + position: 2, + name: 'Deploy QR Code', + text: 'Download and place your QR code on marketing materials, products, or digital platforms', + }, + { + '@type': 'HowToStep', + position: 3, + name: 'Monitor Analytics', + text: 'View real-time scan data including location, device, and time patterns in your dashboard', + url: 'https://www.qrmaster.net/analytics', + }, + { + '@type': 'HowToStep', + position: 4, + name: 'Optimize Campaigns', + text: 'Use insights to optimize placement, timing, and targeting of your QR code campaigns', + }, + ], + }; + + const breadcrumbItems: BreadcrumbItem[] = [ + { name: 'Home', url: '/' }, + { name: 'QR Code Tracking', url: '/qr-code-tracking' }, + ]; + + return ( + <> + <SeoJsonLd data={[softwareSchema, howToSchema, breadcrumbSchema(breadcrumbItems)]} /> + <div className="min-h-screen bg-white"> + {/* Hero Section */} + <section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <Breadcrumbs items={breadcrumbItems} /> + <div className="grid lg:grid-cols-2 gap-12 items-center"> + <div className="space-y-8"> + <div className="inline-flex items-center space-x-2 bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-semibold"> + <span>📊</span> + <span>Free QR Code Tracking</span> + </div> + + <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> + Track Every QR Code Scan with Powerful Analytics + </h1> + + <p className="text-xl text-gray-600 leading-relaxed"> + Monitor your QR code performance in real-time. Get detailed insights on location, device, time, and user behavior. Make data-driven decisions with our free tracking software. + </p> + + <div className="flex flex-col sm:flex-row gap-4"> + <Link href="/signup"> + <Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> + Start Tracking Free + </Button> + </Link> + <Link href="/create"> + <Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto"> + Create Trackable QR Code + </Button> + </Link> + </div> + + <div className="flex items-center space-x-6 text-sm text-gray-600"> + <div className="flex items-center space-x-2"> + <svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + <span>No credit card required</span> + </div> + <div className="flex items-center space-x-2"> + <svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + <span>Unlimited scans</span> + </div> + </div> + </div> + + {/* Analytics Preview */} + <div className="relative"> + <Card className="p-6 shadow-2xl"> + <h3 className="font-semibold text-lg mb-4">Live Analytics Dashboard</h3> + <div className="space-y-4"> + <div className="flex justify-between items-center pb-3 border-b"> + <span className="text-gray-600">Total Scans</span> + <span className="text-2xl font-bold text-primary-600">12,547</span> + </div> + <div className="flex justify-between items-center pb-3 border-b"> + <span className="text-gray-600">Unique Users</span> + <span className="text-2xl font-bold text-primary-600">8,392</span> + </div> + <div className="flex justify-between items-center pb-3 border-b"> + <span className="text-gray-600">Top Location</span> + <span className="font-semibold">đŸ‡©đŸ‡Ș Germany</span> + </div> + <div className="flex justify-between items-center"> + <span className="text-gray-600">Top Device</span> + <span className="font-semibold">đŸ“± iPhone</span> + </div> + </div> + </Card> + <div className="absolute -top-4 -right-4 bg-green-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg animate-pulse"> + Live Updates + </div> + </div> + </div> + </div> + </section> + + {/* Tracking Features */} + <section className="py-20 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + Powerful QR Code Tracking Features + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + Get complete visibility into your QR code performance with our comprehensive analytics suite + </p> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> + {trackingFeatures.map((feature, index) => ( + <Card key={index} className="p-6 hover:shadow-lg transition-shadow"> + <div className="text-4xl mb-4">{feature.icon}</div> + <h3 className="text-xl font-semibold text-gray-900 mb-2"> + {feature.title} + </h3> + <p className="text-gray-600"> + {feature.description} + </p> + </Card> + ))} + </div> + </div> + </section> + + {/* Use Cases */} + <section className="py-20"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + QR Code Tracking Use Cases + </h2> + <p className="text-xl text-gray-600 max-w-3xl mx-auto"> + See how businesses use QR code tracking to improve their operations + </p> + </div> + + <div className="grid md:grid-cols-2 gap-8"> + {useCases.map((useCase, index) => ( + <Card key={index} className="p-8"> + <h3 className="text-2xl font-bold text-gray-900 mb-3"> + {useCase.title} + </h3> + <p className="text-gray-600 mb-6"> + {useCase.description} + </p> + <ul className="space-y-2"> + {useCase.benefits.map((benefit, idx) => ( + <li key={idx} className="flex items-center space-x-2"> + <svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + <span className="text-gray-700">{benefit}</span> + </li> + ))} + </ul> + </Card> + ))} + </div> + </div> + </section> + + {/* Comparison Table */} + <section className="py-20 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> + <div className="text-center mb-16"> + <h2 className="text-4xl font-bold text-gray-900 mb-4"> + QR Master vs Free Tools + </h2> + <p className="text-xl text-gray-600"> + See why businesses choose QR Master for QR code tracking + </p> + </div> + + <Card className="overflow-hidden"> + <table className="w-full"> + <thead className="bg-gray-100"> + <tr> + <th className="px-6 py-4 text-left text-gray-900 font-semibold">Feature</th> + <th className="px-6 py-4 text-center text-gray-900 font-semibold">Free Tools</th> + <th className="px-6 py-4 text-center text-primary-600 font-semibold">QR Master</th> + </tr> + </thead> + <tbody className="divide-y divide-gray-200"> + {comparisonData.map((row, index) => ( + <tr key={index}> + <td className="px-6 py-4 text-gray-900 font-medium">{row.feature}</td> + <td className="px-6 py-4 text-center"> + {typeof row.free === 'boolean' ? ( + row.free ? ( + <span className="text-green-500 text-2xl">✓</span> + ) : ( + <span className="text-red-500 text-2xl">✗</span> + ) + ) : ( + <span className="text-gray-600">{row.free}</span> + )} + </td> + <td className="px-6 py-4 text-center"> + {typeof row.qrMaster === 'boolean' ? ( + <span className="text-green-500 text-2xl">✓</span> + ) : ( + <span className="text-primary-600 font-semibold">{row.qrMaster}</span> + )} + </td> + </tr> + ))} + </tbody> + </table> + </Card> + </div> + </section> + + {/* CTA Section */} + <section className="py-20 bg-gradient-to-r from-primary-600 to-purple-600 text-white"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl text-center"> + <h2 className="text-4xl font-bold mb-6"> + Start Tracking Your QR Codes Today + </h2> + <p className="text-xl mb-8 text-primary-100"> + Join thousands of businesses using QR Master to track and optimize their QR code campaigns + </p> + <div className="flex flex-col sm:flex-row gap-4 justify-center"> + <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"> + Create Free Account + </Button> + </Link> + <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"> + View Pricing + </Button> + </Link> + </div> + </div> + </section> + </div> + </> + ); +} diff --git a/src/app/api/qrs/route.ts b/src/app/api/qrs/route.ts index 2295d23..4b61b24 100644 --- a/src/app/api/qrs/route.ts +++ b/src/app/api/qrs/route.ts @@ -1,239 +1,239 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; -import { db } from '@/lib/db'; -import { generateSlug } from '@/lib/hash'; -import { createQRSchema, validateRequest } from '@/lib/validationSchemas'; -import { csrfProtection } from '@/lib/csrf'; -import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; - -// GET /api/qrs - List user's QR codes -export async function GET(request: NextRequest) { - try { - const userId = cookies().get('userId')?.value; - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const qrCodes = await db.qRCode.findMany({ - where: { userId }, - include: { - _count: { - select: { scans: true }, - }, - scans: { - where: { isUnique: true }, - select: { id: true }, - }, - }, - orderBy: { createdAt: 'desc' }, - }); - - // Transform the data - const transformed = qrCodes.map(qr => ({ - ...qr, - scans: qr._count.scans, - uniqueScans: qr.scans.length, // Count of scans where isUnique=true - _count: undefined, - })); - - return NextResponse.json(transformed); - } catch (error) { - console.error('Error fetching QR codes:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} - -// Plan limits -const PLAN_LIMITS = { - FREE: 3, - PRO: 50, - BUSINESS: 500, -}; - -// POST /api/qrs - Create a new QR code -export async function POST(request: NextRequest) { - try { - // CSRF Protection - const csrfCheck = csrfProtection(request); - if (!csrfCheck.valid) { - return NextResponse.json({ error: csrfCheck.error }, { status: 403 }); - } - - const userId = cookies().get('userId')?.value; - console.log('POST /api/qrs - userId from cookie:', userId); - - // Rate Limiting (user-based) - const clientId = userId || getClientIdentifier(request); - const rateLimitResult = rateLimit(clientId, RateLimits.QR_CREATE); - - if (!rateLimitResult.success) { - return NextResponse.json( - { - error: 'Too many requests. Please try again later.', - retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000) - }, - { - status: 429, - headers: { - 'X-RateLimit-Limit': rateLimitResult.limit.toString(), - 'X-RateLimit-Remaining': rateLimitResult.remaining.toString(), - 'X-RateLimit-Reset': rateLimitResult.reset.toString(), - } - } - ); - } - - if (!userId) { - return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 }); - } - - // Check if user exists and get their plan - const user = await db.user.findUnique({ - where: { id: userId }, - select: { plan: true }, - }); - - console.log('User exists:', !!user); - - if (!user) { - return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 }); - } - - const body = await request.json(); - console.log('Request body:', body); - - // Validate request body with Zod (only for non-static QRs or simplified validation) - // Note: Static QRs have complex nested content structure, so we do basic validation - if (!body.isStatic) { - const validation = await validateRequest(createQRSchema, body); - if (!validation.success) { - return NextResponse.json(validation.error, { status: 400 }); - } - } - - // Check if this is a static QR request - const isStatic = body.isStatic === true; - - // Only check limits for DYNAMIC QR codes (static QR codes are unlimited) - if (!isStatic) { - // Count existing dynamic QR codes - const dynamicQRCount = await db.qRCode.count({ - where: { - userId, - type: 'DYNAMIC', - }, - }); - - const userPlan = user.plan || 'FREE'; - const limit = PLAN_LIMITS[userPlan as keyof typeof PLAN_LIMITS] || PLAN_LIMITS.FREE; - - if (dynamicQRCount >= limit) { - return NextResponse.json( - { - error: 'Limit reached', - message: `You have reached the limit of ${limit} dynamic QR codes for your ${userPlan} plan. Please upgrade to create more.`, - currentCount: dynamicQRCount, - limit, - plan: userPlan, - }, - { status: 403 } - ); - } - } - - let enrichedContent = body.content; - - // For STATIC QR codes, calculate what the QR should contain - if (isStatic) { - let qrContent = ''; - switch (body.contentType) { - case 'URL': - qrContent = body.content.url; - break; - case 'PHONE': - qrContent = `tel:${body.content.phone}`; - break; - case 'SMS': - qrContent = `sms:${body.content.phone}${body.content.message ? `?body=${encodeURIComponent(body.content.message)}` : ''}`; - break; - case 'VCARD': - qrContent = `BEGIN:VCARD -VERSION:3.0 -FN:${body.content.firstName || ''} ${body.content.lastName || ''} -N:${body.content.lastName || ''};${body.content.firstName || ''};;; -${body.content.organization ? `ORG:${body.content.organization}` : ''} -${body.content.title ? `TITLE:${body.content.title}` : ''} -${body.content.email ? `EMAIL:${body.content.email}` : ''} -${body.content.phone ? `TEL:${body.content.phone}` : ''} -END:VCARD`; - break; - case 'GEO': - const lat = body.content.latitude || 0; - const lon = body.content.longitude || 0; - const label = body.content.label ? `?q=${encodeURIComponent(body.content.label)}` : ''; - qrContent = `geo:${lat},${lon}${label}`; - break; - case 'TEXT': - qrContent = body.content.text; - break; - case 'WHATSAPP': - qrContent = `https://wa.me/${body.content.phone}${body.content.message ? `?text=${encodeURIComponent(body.content.message)}` : ''}`; - break; - case 'PDF': - qrContent = body.content.fileUrl || 'https://example.com/file.pdf'; - break; - case 'APP': - qrContent = body.content.fallbackUrl || body.content.iosUrl || body.content.androidUrl || 'https://example.com'; - break; - case 'COUPON': - qrContent = `Coupon: ${body.content.code || 'CODE'} - ${body.content.discount || 'Discount'}`; - break; - case 'FEEDBACK': - qrContent = body.content.feedbackUrl || 'https://example.com/feedback'; - break; - default: - qrContent = body.content.url || 'https://example.com'; - } - - // Add qrContent to the content object - enrichedContent = { - ...body.content, - qrContent // This is what the QR code should actually contain - }; - } - - // Generate slug for the QR code - const slug = generateSlug(body.title); - - // Create QR code - const qrCode = await db.qRCode.create({ - data: { - userId, - title: body.title, - type: isStatic ? 'STATIC' : 'DYNAMIC', - contentType: body.contentType, - content: enrichedContent, - tags: body.tags || [], - style: body.style || { - foregroundColor: '#000000', - backgroundColor: '#FFFFFF', - cornerStyle: 'square', - size: 200, - }, - slug, - status: 'ACTIVE', - }, - }); - - return NextResponse.json(qrCode); - } catch (error) { - console.error('Error creating QR code:', error); - return NextResponse.json( - { error: 'Internal server error', details: String(error) }, - { status: 500 } - ); - } +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { db } from '@/lib/db'; +import { generateSlug } from '@/lib/hash'; +import { createQRSchema, validateRequest } from '@/lib/validationSchemas'; +import { csrfProtection } from '@/lib/csrf'; +import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; + +// GET /api/qrs - List user's QR codes +export async function GET(request: NextRequest) { + try { + const userId = cookies().get('userId')?.value; + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const qrCodes = await db.qRCode.findMany({ + where: { userId }, + include: { + _count: { + select: { scans: true }, + }, + scans: { + where: { isUnique: true }, + select: { id: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + // Transform the data + const transformed = qrCodes.map(qr => ({ + ...qr, + scans: qr._count.scans, + uniqueScans: qr.scans.length, // Count of scans where isUnique=true + _count: undefined, + })); + + return NextResponse.json(transformed); + } catch (error) { + console.error('Error fetching QR codes:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// Plan limits +const PLAN_LIMITS = { + FREE: 3, + PRO: 50, + BUSINESS: 500, +}; + +// POST /api/qrs - Create a new QR code +export async function POST(request: NextRequest) { + try { + // CSRF Protection + const csrfCheck = csrfProtection(request); + if (!csrfCheck.valid) { + return NextResponse.json({ error: csrfCheck.error }, { status: 403 }); + } + + const userId = cookies().get('userId')?.value; + console.log('POST /api/qrs - userId from cookie:', userId); + + // Rate Limiting (user-based) + const clientId = userId || getClientIdentifier(request); + const rateLimitResult = rateLimit(clientId, RateLimits.QR_CREATE); + + if (!rateLimitResult.success) { + return NextResponse.json( + { + error: 'Too many requests. Please try again later.', + retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000) + }, + { + status: 429, + headers: { + 'X-RateLimit-Limit': rateLimitResult.limit.toString(), + 'X-RateLimit-Remaining': rateLimitResult.remaining.toString(), + 'X-RateLimit-Reset': rateLimitResult.reset.toString(), + } + } + ); + } + + if (!userId) { + return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 }); + } + + // Check if user exists and get their plan + const user = await db.user.findUnique({ + where: { id: userId }, + select: { plan: true }, + }); + + console.log('User exists:', !!user); + + if (!user) { + return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 }); + } + + const body = await request.json(); + console.log('Request body:', body); + + // Validate request body with Zod (only for non-static QRs or simplified validation) + // Note: Static QRs have complex nested content structure, so we do basic validation + if (!body.isStatic) { + const validation = await validateRequest(createQRSchema, body); + if (!validation.success) { + return NextResponse.json(validation.error, { status: 400 }); + } + } + + // Check if this is a static QR request + const isStatic = body.isStatic === true; + + // Only check limits for DYNAMIC QR codes (static QR codes are unlimited) + if (!isStatic) { + // Count existing dynamic QR codes + const dynamicQRCount = await db.qRCode.count({ + where: { + userId, + type: 'DYNAMIC', + }, + }); + + const userPlan = user.plan || 'FREE'; + const limit = PLAN_LIMITS[userPlan as keyof typeof PLAN_LIMITS] || PLAN_LIMITS.FREE; + + if (dynamicQRCount >= limit) { + return NextResponse.json( + { + error: 'Limit reached', + message: `You have reached the limit of ${limit} dynamic QR codes for your ${userPlan} plan. Please upgrade to create more.`, + currentCount: dynamicQRCount, + limit, + plan: userPlan, + }, + { status: 403 } + ); + } + } + + let enrichedContent = body.content; + + // For STATIC QR codes, calculate what the QR should contain + if (isStatic) { + let qrContent = ''; + switch (body.contentType) { + case 'URL': + qrContent = body.content.url; + break; + case 'PHONE': + qrContent = `tel:${body.content.phone}`; + break; + case 'SMS': + qrContent = `sms:${body.content.phone}${body.content.message ? `?body=${encodeURIComponent(body.content.message)}` : ''}`; + break; + case 'VCARD': + qrContent = `BEGIN:VCARD +VERSION:3.0 +FN:${body.content.firstName || ''} ${body.content.lastName || ''} +N:${body.content.lastName || ''};${body.content.firstName || ''};;; +${body.content.organization ? `ORG:${body.content.organization}` : ''} +${body.content.title ? `TITLE:${body.content.title}` : ''} +${body.content.email ? `EMAIL:${body.content.email}` : ''} +${body.content.phone ? `TEL:${body.content.phone}` : ''} +END:VCARD`; + break; + case 'GEO': + const lat = body.content.latitude || 0; + const lon = body.content.longitude || 0; + const label = body.content.label ? `?q=${encodeURIComponent(body.content.label)}` : ''; + qrContent = `geo:${lat},${lon}${label}`; + break; + case 'TEXT': + qrContent = body.content.text; + break; + case 'WHATSAPP': + qrContent = `https://wa.me/${body.content.phone}${body.content.message ? `?text=${encodeURIComponent(body.content.message)}` : ''}`; + break; + case 'PDF': + qrContent = body.content.fileUrl || 'https://example.com/file.pdf'; + break; + case 'APP': + qrContent = body.content.fallbackUrl || body.content.iosUrl || body.content.androidUrl || 'https://example.com'; + break; + case 'COUPON': + qrContent = `Coupon: ${body.content.code || 'CODE'} - ${body.content.discount || 'Discount'}`; + break; + case 'FEEDBACK': + qrContent = body.content.feedbackUrl || 'https://example.com/feedback'; + break; + default: + qrContent = body.content.url || 'https://example.com'; + } + + // Add qrContent to the content object + enrichedContent = { + ...body.content, + qrContent // This is what the QR code should actually contain + }; + } + + // Generate slug for the QR code + const slug = generateSlug(body.title); + + // Create QR code + const qrCode = await db.qRCode.create({ + data: { + userId, + title: body.title, + type: isStatic ? 'STATIC' : 'DYNAMIC', + contentType: body.contentType, + content: enrichedContent, + tags: body.tags || [], + style: body.style || { + foregroundColor: '#000000', + backgroundColor: '#FFFFFF', + cornerStyle: 'square', + size: 200, + }, + slug, + status: 'ACTIVE', + }, + }); + + return NextResponse.json(qrCode); + } catch (error) { + console.error('Error creating QR code:', error); + return NextResponse.json( + { error: 'Internal server error', details: String(error) }, + { status: 500 } + ); + } } \ No newline at end of file diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 0000000..856202d --- /dev/null +++ b/src/app/api/upload/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { uploadFileToR2 } from '@/lib/r2'; +import { env } from '@/lib/env'; + +export async function POST(request: NextRequest) { + try { + // 1. Authentication Check + const session = await getServerSession(authOptions); + if (!session || !session.user) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + // 2. Parse Form Data + const formData = await request.formData(); + const file = formData.get('file') as File | null; + + if (!file) { + return NextResponse.json( + { error: 'No file provided' }, + { status: 400 } + ); + } + + // 3. Validation + // Check file size (default 10MB) + const MAX_SIZE = parseInt(env.MAX_UPLOAD_SIZE || '10485760'); + if (file.size > MAX_SIZE) { + return NextResponse.json( + { error: `File too large. Maximum size: ${MAX_SIZE / 1024 / 1024}MB` }, + { status: 400 } + ); + } + + // Check file type (allow images and PDFs) + const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + return NextResponse.json( + { error: 'Invalid file type. Only PDF and Images are allowed.' }, + { status: 400 } + ); + } + + // 4. Upload to R2 + const buffer = Buffer.from(await file.arrayBuffer()); + const publicUrl = await uploadFileToR2(buffer, file.name, file.type); + + // 5. Success + return NextResponse.json({ + success: true, + url: publicUrl, + filename: file.name, + type: file.type + }); + + } catch (error) { + console.error('Upload error:', error); + return NextResponse.json( + { error: 'Internal server error during upload' }, + { status: 500 } + ); + } +} diff --git a/src/app/error.tsx b/src/app/error.tsx index 1d99b54..ef856a0 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,119 +1,119 @@ -'use client'; - -import React, { useEffect } from 'react'; -import Link from 'next/link'; -import { Button } from '@/components/ui/Button'; - -export default function Error({ - error, - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - useEffect(() => { - // Log the error to an error reporting service - console.error('Error:', error); - }, [error]); - - return ( - <div className="min-h-screen bg-white flex items-center justify-center px-4"> - <div className="max-w-2xl w-full text-center"> - {/* Error Icon */} - <div className="mb-8"> - <div className="inline-flex items-center justify-center w-24 h-24 bg-red-100 rounded-full mb-6"> - <svg - className="w-12 h-12 text-red-600" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" - /> - </svg> - </div> - - {/* Error Text */} - <h1 className="text-6xl md:text-8xl font-bold text-gray-900 mb-4">500</h1> - <h2 className="text-2xl md:text-3xl font-semibold text-gray-700 mb-4"> - Something Went Wrong - </h2> - <p className="text-lg text-gray-600 mb-8 max-w-md mx-auto"> - We're sorry, but something unexpected happened. Our team has been notified and is working on a fix. - </p> - - {/* Error Details (only in development) */} - {process.env.NODE_ENV === 'development' && error.message && ( - <div className="mb-8 p-4 bg-red-50 border border-red-200 rounded-lg text-left"> - <p className="text-sm font-mono text-red-800 break-all"> - <strong>Error:</strong> {error.message} - </p> - {error.digest && ( - <p className="text-sm font-mono text-red-600 mt-2"> - <strong>Digest:</strong> {error.digest} - </p> - )} - </div> - )} - </div> - - {/* Action Buttons */} - <div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> - <Button size="lg" onClick={reset}> - <svg - className="w-5 h-5 mr-2" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" - /> - </svg> - Try Again - </Button> - - <Link href="/"> - <Button variant="outline" size="lg"> - <svg - className="w-5 h-5 mr-2" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" - /> - </svg> - Go Home - </Button> - </Link> - </div> - - {/* Help Text */} - <div className="mt-12 pt-8 border-t border-gray-200"> - <p className="text-sm text-gray-500"> - If this problem persists, please{' '} - <Link href="/#faq" className="text-primary-600 hover:text-primary-700 font-medium"> - check our FAQ - </Link> - {' '}or contact support. - </p> - </div> - </div> - </div> - ); -} +'use client'; + +import React, { useEffect } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error('Error:', error); + }, [error]); + + return ( + <div className="min-h-screen bg-white flex items-center justify-center px-4"> + <div className="max-w-2xl w-full text-center"> + {/* Error Icon */} + <div className="mb-8"> + <div className="inline-flex items-center justify-center w-24 h-24 bg-red-100 rounded-full mb-6"> + <svg + className="w-12 h-12 text-red-600" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" + /> + </svg> + </div> + + {/* Error Text */} + <h1 className="text-6xl md:text-8xl font-bold text-gray-900 mb-4">500</h1> + <h2 className="text-2xl md:text-3xl font-semibold text-gray-700 mb-4"> + Something Went Wrong + </h2> + <p className="text-lg text-gray-600 mb-8 max-w-md mx-auto"> + We're sorry, but something unexpected happened. Our team has been notified and is working on a fix. + </p> + + {/* Error Details (only in development) */} + {process.env.NODE_ENV === 'development' && error.message && ( + <div className="mb-8 p-4 bg-red-50 border border-red-200 rounded-lg text-left"> + <p className="text-sm font-mono text-red-800 break-all"> + <strong>Error:</strong> {error.message} + </p> + {error.digest && ( + <p className="text-sm font-mono text-red-600 mt-2"> + <strong>Digest:</strong> {error.digest} + </p> + )} + </div> + )} + </div> + + {/* Action Buttons */} + <div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> + <Button size="lg" onClick={reset}> + <svg + className="w-5 h-5 mr-2" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" + /> + </svg> + Try Again + </Button> + + <Link href="/"> + <Button variant="outline" size="lg"> + <svg + className="w-5 h-5 mr-2" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" + /> + </svg> + Go Home + </Button> + </Link> + </div> + + {/* Help Text */} + <div className="mt-12 pt-8 border-t border-gray-200"> + <p className="text-sm text-gray-500"> + If this problem persists, please{' '} + <Link href="/#faq" className="text-primary-600 hover:text-primary-700 font-medium"> + check our FAQ + </Link> + {' '}or contact support. + </p> + </div> + </div> + </div> + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5b9b003..ea1a54b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,72 +1,72 @@ -import type { Metadata } from 'next'; -import { Suspense } from 'react'; -import '@/styles/globals.css'; -import { ToastContainer } from '@/components/ui/Toast'; -import AuthProvider from '@/components/SessionProvider'; -import { PostHogProvider } from '@/components/PostHogProvider'; -import CookieBanner from '@/components/CookieBanner'; - -const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true'; - -export const metadata: Metadata = { - metadataBase: new URL('https://www.qrmaster.net'), - title: { - default: 'QR Master – Smart QR Generator & Analytics', - template: '%s | QR Master', - }, - description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.', - keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics, branded QR, bulk QR generator', - robots: isIndexable - ? { index: true, follow: true } - : { index: false, follow: false }, - icons: { - icon: [ - { url: '/favicon.svg', type: 'image/svg+xml' }, - { url: '/logo.svg', type: 'image/svg+xml' }, - ], - apple: '/logo.svg', - }, - twitter: { - card: 'summary_large_image', - site: '@qrmaster', - images: ['https://www.qrmaster.net/static/og-image.png'], - }, - openGraph: { - type: 'website', - siteName: 'QR Master', - title: 'QR Master – Smart QR Generator & Analytics', - description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.', - url: 'https://www.qrmaster.net', - images: [ - { - url: 'https://www.qrmaster.net/static/og-image.png', - width: 1200, - height: 630, - alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform', - }, - ], - locale: 'en_US', - }, -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - <html lang="en"> - <body className="font-sans"> - <Suspense fallback={null}> - <PostHogProvider> - <AuthProvider> - {children} - </AuthProvider> - <CookieBanner /> - <ToastContainer /> - </PostHogProvider> - </Suspense> - </body> - </html> - ); +import type { Metadata } from 'next'; +import { Suspense } from 'react'; +import '@/styles/globals.css'; +import { ToastContainer } from '@/components/ui/Toast'; +import AuthProvider from '@/components/SessionProvider'; +import { PostHogProvider } from '@/components/PostHogProvider'; +import CookieBanner from '@/components/CookieBanner'; + +const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true'; + +export const metadata: Metadata = { + metadataBase: new URL('https://www.qrmaster.net'), + title: { + default: 'QR Master – Smart QR Generator & Analytics', + template: '%s | QR Master', + }, + description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.', + keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics, branded QR, bulk QR generator', + robots: isIndexable + ? { index: true, follow: true } + : { index: false, follow: false }, + icons: { + icon: [ + { url: '/favicon.svg', type: 'image/svg+xml' }, + { url: '/logo.svg', type: 'image/svg+xml' }, + ], + apple: '/logo.svg', + }, + twitter: { + card: 'summary_large_image', + site: '@qrmaster', + images: ['https://www.qrmaster.net/static/og-image.png'], + }, + openGraph: { + type: 'website', + siteName: 'QR Master', + title: 'QR Master – Smart QR Generator & Analytics', + description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.', + url: 'https://www.qrmaster.net', + images: [ + { + url: 'https://www.qrmaster.net/static/og-image.png', + width: 1200, + height: 630, + alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform', + }, + ], + locale: 'en_US', + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <html lang="en"> + <body className="font-sans"> + <Suspense fallback={null}> + <PostHogProvider> + <AuthProvider> + {children} + </AuthProvider> + <CookieBanner /> + <ToastContainer /> + </PostHogProvider> + </Suspense> + </body> + </html> + ); } \ No newline at end of file diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index b2e7b3f..48190b5 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,63 +1,63 @@ -import React from 'react'; -import Link from 'next/link'; -import { Button } from '@/components/ui/Button'; - -export default function NotFound() { - return ( - <div className="min-h-screen bg-white flex items-center justify-center px-4"> - <div className="max-w-2xl w-full text-center"> - {/* 404 Icon */} - <div className="mb-8"> - <div className="inline-flex items-center justify-center w-24 h-24 bg-primary-100 rounded-full mb-6"> - <svg - className="w-12 h-12 text-primary-600" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" - /> - </svg> - </div> - - {/* 404 Text */} - <h1 className="text-6xl md:text-8xl font-bold text-gray-900 mb-4">404</h1> - <h2 className="text-2xl md:text-3xl font-semibold text-gray-700 mb-4"> - Page Not Found - </h2> - <p className="text-lg text-gray-600 mb-8 max-w-md mx-auto"> - Sorry, we couldn't find the page you're looking for. It might have been moved or deleted. - </p> - </div> - - {/* Action Button */} - <div className="flex justify-center"> - <Link href="/"> - <Button size="lg"> - <svg - className="w-5 h-5 mr-2" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" - /> - </svg> - Back to Home - </Button> - </Link> - </div> - </div> - </div> - ); -} +import React from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; + +export default function NotFound() { + return ( + <div className="min-h-screen bg-white flex items-center justify-center px-4"> + <div className="max-w-2xl w-full text-center"> + {/* 404 Icon */} + <div className="mb-8"> + <div className="inline-flex items-center justify-center w-24 h-24 bg-primary-100 rounded-full mb-6"> + <svg + className="w-12 h-12 text-primary-600" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + /> + </svg> + </div> + + {/* 404 Text */} + <h1 className="text-6xl md:text-8xl font-bold text-gray-900 mb-4">404</h1> + <h2 className="text-2xl md:text-3xl font-semibold text-gray-700 mb-4"> + Page Not Found + </h2> + <p className="text-lg text-gray-600 mb-8 max-w-md mx-auto"> + Sorry, we couldn't find the page you're looking for. It might have been moved or deleted. + </p> + </div> + + {/* Action Button */} + <div className="flex justify-center"> + <Link href="/"> + <Button size="lg"> + <svg + className="w-5 h-5 mr-2" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" + /> + </svg> + Back to Home + </Button> + </Link> + </div> + </div> + </div> + ); +} diff --git a/src/app/r/[slug]/route.ts b/src/app/r/[slug]/route.ts index b4a0c01..10473e8 100644 --- a/src/app/r/[slug]/route.ts +++ b/src/app/r/[slug]/route.ts @@ -1,240 +1,240 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { db } from '@/lib/db'; -import { hashIP } from '@/lib/hash'; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ slug: string }> } -) { - try { - const { slug } = await params; - - // Fetch QR code by slug - const qrCode = await db.qRCode.findUnique({ - where: { slug }, - select: { - id: true, - content: true, - contentType: true, - }, - }); - - if (!qrCode) { - return new NextResponse('QR Code not found', { status: 404 }); - } - - // Track scan (fire and forget) - trackScan(qrCode.id, request).catch(console.error); - - // Determine destination URL - let destination = ''; - const content = qrCode.content as any; - - switch (qrCode.contentType) { - case 'URL': - destination = content.url || 'https://example.com'; - break; - case 'PHONE': - destination = `tel:${content.phone}`; - break; - case 'SMS': - destination = `sms:${content.phone}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`; - break; - case 'WHATSAPP': - destination = `https://wa.me/${content.phone}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`; - break; - case 'VCARD': - // For vCard, redirect to display page - const baseUrlVcard = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; - destination = `${baseUrlVcard}/vcard?firstName=${encodeURIComponent(content.firstName || '')}&lastName=${encodeURIComponent(content.lastName || '')}&email=${encodeURIComponent(content.email || '')}&phone=${encodeURIComponent(content.phone || '')}&organization=${encodeURIComponent(content.organization || '')}&title=${encodeURIComponent(content.title || '')}`; - break; - case 'GEO': - // For location, redirect to Google Maps (works on desktop and mobile) - const lat = content.latitude || 0; - const lon = content.longitude || 0; - destination = `https://maps.google.com/?q=${lat},${lon}`; - break; - case 'TEXT': - // For plain text, redirect to a display page - const baseUrlText = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; - destination = `${baseUrlText}/display?text=${encodeURIComponent(content.text || '')}`; - break; - case 'PDF': - // Direct link to file - destination = content.fileUrl || 'https://example.com/file.pdf'; - break; - case 'APP': - // Smart device detection for app stores - const userAgent = request.headers.get('user-agent') || ''; - const isIOS = /iphone|ipad|ipod/i.test(userAgent); - const isAndroid = /android/i.test(userAgent); - - if (isIOS && content.iosUrl) { - destination = content.iosUrl; - } else if (isAndroid && content.androidUrl) { - destination = content.androidUrl; - } else { - destination = content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com'; - } - break; - case 'COUPON': - // Redirect to coupon display page - const baseUrlCoupon = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; - destination = `${baseUrlCoupon}/coupon/${slug}`; - break; - case 'FEEDBACK': - // Redirect to feedback form page - const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; - destination = `${baseUrlFeedback}/feedback/${slug}`; - break; - default: - destination = 'https://example.com'; - } - - // Preserve UTM parameters - const searchParams = request.nextUrl.searchParams; - const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; - const preservedParams = new URLSearchParams(); - - utmParams.forEach(param => { - const value = searchParams.get(param); - if (value) { - preservedParams.set(param, value); - } - }); - - // Add preserved params to destination - if (preservedParams.toString() && destination.startsWith('http')) { - const separator = destination.includes('?') ? '&' : '?'; - destination = `${destination}${separator}${preservedParams.toString()}`; - } - - // Return 307 redirect (temporary redirect that preserves method) - return NextResponse.redirect(destination, { status: 307 }); - } catch (error) { - console.error('QR redirect error:', error); - return new NextResponse('Internal server error', { status: 500 }); - } -} - -async function trackScan(qrId: string, request: NextRequest) { - try { - const userAgent = request.headers.get('user-agent') || ''; - const referer = request.headers.get('referer') || ''; - const ip = request.headers.get('x-forwarded-for') || - request.headers.get('x-real-ip') || - 'unknown'; - - // Check DNT header - const dnt = request.headers.get('dnt'); - if (dnt === '1') { - // Respect Do Not Track - only increment counter - await db.qRScan.create({ - data: { - qrId, - ipHash: 'dnt', - isUnique: false, - }, - }); - return; - } - - // Hash IP for privacy - const ipHash = hashIP(ip); - - // Device Detection Logic: - // 1. Windows or Linux -> Always Desktop - // 2. Explicit iPad/Tablet keywords -> Tablet - // 3. Mac + Chrome browser -> Desktop (real Mac users often use Chrome) - // 4. Mac + Safari + No Referrer -> Likely iPad scanning a QR code - // 5. Mobile keywords -> Mobile - // 6. Everything else -> Desktop - - const isWindows = /windows/i.test(userAgent); - const isLinux = /linux/i.test(userAgent) && !/android/i.test(userAgent); - const isExplicitTablet = /tablet|ipad|playbook|silk/i.test(userAgent); - const isAndroidTablet = /android/i.test(userAgent) && !/mobile/i.test(userAgent); - const isMacintosh = /macintosh/i.test(userAgent); - const isChrome = /chrome/i.test(userAgent); - const isSafari = /safari/i.test(userAgent) && !isChrome; - const hasReferrer = !!referer; - - // iPad in desktop mode: Mac + Safari (no Chrome) + No Referrer (physical scan) - const isLikelyiPadScan = isMacintosh && isSafari && !hasReferrer; - - let device: string; - if (isWindows || isLinux) { - device = 'desktop'; - } else if (isExplicitTablet || isAndroidTablet || isLikelyiPadScan) { - device = 'tablet'; - } else if (/mobile|iphone/i.test(userAgent)) { - device = 'mobile'; - } else if (isMacintosh && isChrome) { - device = 'desktop'; // Mac with Chrome = real desktop - } else if (isMacintosh && hasReferrer) { - device = 'desktop'; // Mac with referrer = probably clicked a link on desktop - } else { - device = 'desktop'; // Default fallback - } - - // Detect OS - let os = 'unknown'; - if (/windows/i.test(userAgent)) os = 'Windows'; - else if (/mac/i.test(userAgent)) os = 'macOS'; - else if (/linux/i.test(userAgent)) os = 'Linux'; - else if (/android/i.test(userAgent)) os = 'Android'; - else if (/ios|iphone|ipad/i.test(userAgent)) os = 'iOS'; - - // Get country from header (Vercel/Cloudflare provide this) - const country = request.headers.get('x-vercel-ip-country') || - request.headers.get('cf-ipcountry') || - 'unknown'; - - // Extract UTM parameters - const searchParams = request.nextUrl.searchParams; - const utmSource = searchParams.get('utm_source'); - const utmMedium = searchParams.get('utm_medium'); - const utmCampaign = searchParams.get('utm_campaign'); - - // Check if this is a unique scan (first scan from this IP + Device today) - // We include a simplified device fingerprint so different devices on same IP count as unique - const deviceFingerprint = hashIP(userAgent.substring(0, 100)); // Hash the user agent for privacy - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const existingScan = await db.qRScan.findFirst({ - where: { - qrId, - ipHash, - userAgent: { - startsWith: userAgent.substring(0, 50), // Match same device type - }, - ts: { - gte: today, - }, - }, - }); - - const isUnique = !existingScan; - - // Create scan record - await db.qRScan.create({ - data: { - qrId, - ipHash, - userAgent: userAgent.substring(0, 255), - device, - os, - country, - referrer: referer.substring(0, 255), - utmSource, - utmMedium, - utmCampaign, - isUnique, - }, - }); - } catch (error) { - console.error('Error tracking scan:', error); - // Don't throw - this is fire and forget - } +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { hashIP } from '@/lib/hash'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ slug: string }> } +) { + try { + const { slug } = await params; + + // Fetch QR code by slug + const qrCode = await db.qRCode.findUnique({ + where: { slug }, + select: { + id: true, + content: true, + contentType: true, + }, + }); + + if (!qrCode) { + return new NextResponse('QR Code not found', { status: 404 }); + } + + // Track scan (fire and forget) + trackScan(qrCode.id, request).catch(console.error); + + // Determine destination URL + let destination = ''; + const content = qrCode.content as any; + + switch (qrCode.contentType) { + case 'URL': + destination = content.url || 'https://example.com'; + break; + case 'PHONE': + destination = `tel:${content.phone}`; + break; + case 'SMS': + destination = `sms:${content.phone}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`; + break; + case 'WHATSAPP': + destination = `https://wa.me/${content.phone}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`; + break; + case 'VCARD': + // For vCard, redirect to display page + const baseUrlVcard = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; + destination = `${baseUrlVcard}/vcard?firstName=${encodeURIComponent(content.firstName || '')}&lastName=${encodeURIComponent(content.lastName || '')}&email=${encodeURIComponent(content.email || '')}&phone=${encodeURIComponent(content.phone || '')}&organization=${encodeURIComponent(content.organization || '')}&title=${encodeURIComponent(content.title || '')}`; + break; + case 'GEO': + // For location, redirect to Google Maps (works on desktop and mobile) + const lat = content.latitude || 0; + const lon = content.longitude || 0; + destination = `https://maps.google.com/?q=${lat},${lon}`; + break; + case 'TEXT': + // For plain text, redirect to a display page + const baseUrlText = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; + destination = `${baseUrlText}/display?text=${encodeURIComponent(content.text || '')}`; + break; + case 'PDF': + // Direct link to file + destination = content.fileUrl || 'https://example.com/file.pdf'; + break; + case 'APP': + // Smart device detection for app stores + const userAgent = request.headers.get('user-agent') || ''; + const isIOS = /iphone|ipad|ipod/i.test(userAgent); + const isAndroid = /android/i.test(userAgent); + + if (isIOS && content.iosUrl) { + destination = content.iosUrl; + } else if (isAndroid && content.androidUrl) { + destination = content.androidUrl; + } else { + destination = content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com'; + } + break; + case 'COUPON': + // Redirect to coupon display page + const baseUrlCoupon = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; + destination = `${baseUrlCoupon}/coupon/${slug}`; + break; + case 'FEEDBACK': + // Redirect to feedback form page + const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; + destination = `${baseUrlFeedback}/feedback/${slug}`; + break; + default: + destination = 'https://example.com'; + } + + // Preserve UTM parameters + const searchParams = request.nextUrl.searchParams; + const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; + const preservedParams = new URLSearchParams(); + + utmParams.forEach(param => { + const value = searchParams.get(param); + if (value) { + preservedParams.set(param, value); + } + }); + + // Add preserved params to destination + if (preservedParams.toString() && destination.startsWith('http')) { + const separator = destination.includes('?') ? '&' : '?'; + destination = `${destination}${separator}${preservedParams.toString()}`; + } + + // Return 307 redirect (temporary redirect that preserves method) + return NextResponse.redirect(destination, { status: 307 }); + } catch (error) { + console.error('QR redirect error:', error); + return new NextResponse('Internal server error', { status: 500 }); + } +} + +async function trackScan(qrId: string, request: NextRequest) { + try { + const userAgent = request.headers.get('user-agent') || ''; + const referer = request.headers.get('referer') || ''; + const ip = request.headers.get('x-forwarded-for') || + request.headers.get('x-real-ip') || + 'unknown'; + + // Check DNT header + const dnt = request.headers.get('dnt'); + if (dnt === '1') { + // Respect Do Not Track - only increment counter + await db.qRScan.create({ + data: { + qrId, + ipHash: 'dnt', + isUnique: false, + }, + }); + return; + } + + // Hash IP for privacy + const ipHash = hashIP(ip); + + // Device Detection Logic: + // 1. Windows or Linux -> Always Desktop + // 2. Explicit iPad/Tablet keywords -> Tablet + // 3. Mac + Chrome browser -> Desktop (real Mac users often use Chrome) + // 4. Mac + Safari + No Referrer -> Likely iPad scanning a QR code + // 5. Mobile keywords -> Mobile + // 6. Everything else -> Desktop + + const isWindows = /windows/i.test(userAgent); + const isLinux = /linux/i.test(userAgent) && !/android/i.test(userAgent); + const isExplicitTablet = /tablet|ipad|playbook|silk/i.test(userAgent); + const isAndroidTablet = /android/i.test(userAgent) && !/mobile/i.test(userAgent); + const isMacintosh = /macintosh/i.test(userAgent); + const isChrome = /chrome/i.test(userAgent); + const isSafari = /safari/i.test(userAgent) && !isChrome; + const hasReferrer = !!referer; + + // iPad in desktop mode: Mac + Safari (no Chrome) + No Referrer (physical scan) + const isLikelyiPadScan = isMacintosh && isSafari && !hasReferrer; + + let device: string; + if (isWindows || isLinux) { + device = 'desktop'; + } else if (isExplicitTablet || isAndroidTablet || isLikelyiPadScan) { + device = 'tablet'; + } else if (/mobile|iphone/i.test(userAgent)) { + device = 'mobile'; + } else if (isMacintosh && isChrome) { + device = 'desktop'; // Mac with Chrome = real desktop + } else if (isMacintosh && hasReferrer) { + device = 'desktop'; // Mac with referrer = probably clicked a link on desktop + } else { + device = 'desktop'; // Default fallback + } + + // Detect OS + let os = 'unknown'; + if (/windows/i.test(userAgent)) os = 'Windows'; + else if (/mac/i.test(userAgent)) os = 'macOS'; + else if (/linux/i.test(userAgent)) os = 'Linux'; + else if (/android/i.test(userAgent)) os = 'Android'; + else if (/ios|iphone|ipad/i.test(userAgent)) os = 'iOS'; + + // Get country from header (Vercel/Cloudflare provide this) + const country = request.headers.get('x-vercel-ip-country') || + request.headers.get('cf-ipcountry') || + 'unknown'; + + // Extract UTM parameters + const searchParams = request.nextUrl.searchParams; + const utmSource = searchParams.get('utm_source'); + const utmMedium = searchParams.get('utm_medium'); + const utmCampaign = searchParams.get('utm_campaign'); + + // Check if this is a unique scan (first scan from this IP + Device today) + // We include a simplified device fingerprint so different devices on same IP count as unique + const deviceFingerprint = hashIP(userAgent.substring(0, 100)); // Hash the user agent for privacy + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const existingScan = await db.qRScan.findFirst({ + where: { + qrId, + ipHash, + userAgent: { + startsWith: userAgent.substring(0, 50), // Match same device type + }, + ts: { + gte: today, + }, + }, + }); + + const isUnique = !existingScan; + + // Create scan record + await db.qRScan.create({ + data: { + qrId, + ipHash, + userAgent: userAgent.substring(0, 255), + device, + os, + country, + referrer: referer.substring(0, 255), + utmSource, + utmMedium, + utmCampaign, + isUnique, + }, + }); + } catch (error) { + console.error('Error tracking scan:', error); + // Don't throw - this is fire and forget + } } \ No newline at end of file diff --git a/src/app/robots.ts b/src/app/robots.ts index 2906c28..e4e1eac 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,21 +1,21 @@ -import { MetadataRoute } from 'next'; - -export default function robots(): MetadataRoute.Robots { - const baseUrl = 'https://www.qrmaster.net'; - - return { - rules: [ - { - userAgent: '*', - allow: '/', - disallow: [ - '/api/', - '/dashboard/', - '/create/', - '/settings/', - ], - }, - ], - sitemap: `${baseUrl}/sitemap.xml`, - }; -} +import { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + const baseUrl = 'https://www.qrmaster.net'; + + return { + rules: [ + { + userAgent: '*', + allow: '/', + disallow: [ + '/api/', + '/dashboard/', + '/create/', + '/settings/', + ], + }, + ], + sitemap: `${baseUrl}/sitemap.xml`, + }; +} diff --git a/src/app/vcard/page.tsx b/src/app/vcard/page.tsx index 9caad7c..f74e59b 100644 --- a/src/app/vcard/page.tsx +++ b/src/app/vcard/page.tsx @@ -1,274 +1,274 @@ -'use client'; - -import React, { useEffect, useState } from 'react'; -import { useSearchParams } from 'next/navigation'; - -export default function VCardPage() { - const searchParams = useSearchParams(); - const [isLoading, setIsLoading] = useState(true); - const [firstName, setFirstName] = useState(''); - const [lastName, setLastName] = useState(''); - const [email, setEmail] = useState(''); - const [phone, setPhone] = useState(''); - const [organization, setOrganization] = useState(''); - const [title, setTitle] = useState(''); - const [hasAutoDownloaded, setHasAutoDownloaded] = useState(false); - - useEffect(() => { - const firstNameParam = searchParams.get('firstName'); - const lastNameParam = searchParams.get('lastName'); - const emailParam = searchParams.get('email'); - const phoneParam = searchParams.get('phone'); - const organizationParam = searchParams.get('organization'); - const titleParam = searchParams.get('title'); - - if (firstNameParam) setFirstName(firstNameParam); - if (lastNameParam) setLastName(lastNameParam); - if (emailParam) setEmail(emailParam); - if (phoneParam) setPhone(phoneParam); - if (organizationParam) setOrganization(organizationParam); - if (titleParam) setTitle(titleParam); - - setIsLoading(false); - }, [searchParams]); - - // Auto-download after 500ms (only once) - useEffect(() => { - if ((firstName || lastName) && !hasAutoDownloaded) { - const timer = setTimeout(() => { - handleSaveContact(); - setHasAutoDownloaded(true); - }, 500); - return () => clearTimeout(timer); - } - }, [firstName, lastName, hasAutoDownloaded]); // eslint-disable-line react-hooks/exhaustive-deps - - const handleSaveContact = () => { - // Generate vCard format - const vCard = `BEGIN:VCARD -VERSION:3.0 -FN:${firstName} ${lastName} -N:${lastName};${firstName};;; -${organization ? `ORG:${organization}` : ''} -${title ? `TITLE:${title}` : ''} -${email ? `EMAIL:${email}` : ''} -${phone ? `TEL:${phone}` : ''} -END:VCARD`; - - // Create a blob and download - const blob = new Blob([vCard], { type: 'text/vcard;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `${firstName}_${lastName}.vcf`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; - - // Show loading or error state - if (isLoading) { - return ( - <div style={{ - minHeight: '100vh', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - backgroundColor: '#ffffff' - }}> - <div style={{ textAlign: 'center' }}> - <div style={{ fontSize: '48px', marginBottom: '16px' }}>đŸ‘€</div> - <p style={{ color: '#666' }}>Loading contact...</p> - </div> - </div> - ); - } - - if (!firstName && !lastName) { - return ( - <div style={{ - minHeight: '100vh', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - padding: '20px', - backgroundColor: '#ffffff' - }}> - <div style={{ - maxWidth: '420px', - width: '100%', - padding: '48px 32px', - textAlign: 'center' - }}> - <div style={{ - fontSize: '64px', - marginBottom: '24px', - opacity: 0.3 - }}>đŸ‘€</div> - <h1 style={{ - fontSize: '22px', - fontWeight: '600', - marginBottom: '12px', - color: '#1a1a1a' - }}>No Contact Found</h1> - <p style={{ - color: '#666', - fontSize: '15px', - lineHeight: '1.6' - }}>This QR code doesn't contain any contact information.</p> - </div> - </div> - ); - } - - return ( - <div style={{ - minHeight: '100vh', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - padding: '20px', - backgroundColor: '#ffffff' - }}> - <div style={{ - maxWidth: '420px', - width: '100%', - padding: '48px 32px' - }}> - {/* Header */} - <div style={{ textAlign: 'center', marginBottom: '32px' }}> - <div style={{ - width: '80px', - height: '80px', - margin: '0 auto 20px', - borderRadius: '50%', - backgroundColor: '#f0f0f0', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '36px' - }}>đŸ‘€</div> - - <h1 style={{ - fontSize: '28px', - fontWeight: '600', - marginBottom: '8px', - color: '#1a1a1a', - letterSpacing: '-0.02em' - }}> - {firstName} {lastName} - </h1> - - {(title || organization) && ( - <p style={{ - color: '#666', - fontSize: '16px', - marginTop: '4px' - }}> - {title && organization && `${title} at ${organization}`} - {title && !organization && title} - {!title && organization && organization} - </p> - )} - </div> - - {/* Contact Details */} - <div style={{ marginBottom: '32px' }}> - {email && ( - <div style={{ - padding: '16px 20px', - backgroundColor: '#f8f8f8', - borderRadius: '12px', - marginBottom: '12px', - border: '1px solid #f0f0f0' - }}> - <div style={{ - fontSize: '11px', - color: '#888', - marginBottom: '6px', - textTransform: 'uppercase', - letterSpacing: '0.5px', - fontWeight: '500' - }}>Email</div> - <a href={`mailto:${email}`} style={{ - fontSize: '15px', - fontWeight: '500', - color: '#2563eb', - textDecoration: 'none', - wordBreak: 'break-all' - }}>{email}</a> - </div> - )} - - {phone && ( - <div style={{ - padding: '16px 20px', - backgroundColor: '#f8f8f8', - borderRadius: '12px', - border: '1px solid #f0f0f0' - }}> - <div style={{ - fontSize: '11px', - color: '#888', - marginBottom: '6px', - textTransform: 'uppercase', - letterSpacing: '0.5px', - fontWeight: '500' - }}>Phone</div> - <a href={`tel:${phone}`} style={{ - fontSize: '15px', - fontWeight: '500', - color: '#2563eb', - textDecoration: 'none' - }}>{phone}</a> - </div> - )} - </div> - - {/* Save Button */} - <button - onClick={handleSaveContact} - style={{ - width: '100%', - padding: '16px', - fontSize: '16px', - fontWeight: '600', - color: 'white', - backgroundColor: '#2563eb', - border: 'none', - borderRadius: '12px', - cursor: 'pointer', - transition: 'all 0.2s', - boxShadow: '0 2px 8px rgba(37, 99, 235, 0.2)' - }} - onMouseEnter={(e) => { - e.currentTarget.style.backgroundColor = '#1d4ed8'; - e.currentTarget.style.transform = 'translateY(-1px)'; - e.currentTarget.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = '#2563eb'; - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.boxShadow = '0 2px 8px rgba(37, 99, 235, 0.2)'; - }} - > - Save to Contacts - </button> - - <p style={{ - textAlign: 'center', - fontSize: '13px', - color: '#999', - marginTop: '16px', - lineHeight: '1.5' - }}> - Add this contact to your address book - </p> - </div> - </div> - ); -} +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; + +export default function VCardPage() { + const searchParams = useSearchParams(); + const [isLoading, setIsLoading] = useState(true); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [phone, setPhone] = useState(''); + const [organization, setOrganization] = useState(''); + const [title, setTitle] = useState(''); + const [hasAutoDownloaded, setHasAutoDownloaded] = useState(false); + + useEffect(() => { + const firstNameParam = searchParams.get('firstName'); + const lastNameParam = searchParams.get('lastName'); + const emailParam = searchParams.get('email'); + const phoneParam = searchParams.get('phone'); + const organizationParam = searchParams.get('organization'); + const titleParam = searchParams.get('title'); + + if (firstNameParam) setFirstName(firstNameParam); + if (lastNameParam) setLastName(lastNameParam); + if (emailParam) setEmail(emailParam); + if (phoneParam) setPhone(phoneParam); + if (organizationParam) setOrganization(organizationParam); + if (titleParam) setTitle(titleParam); + + setIsLoading(false); + }, [searchParams]); + + // Auto-download after 500ms (only once) + useEffect(() => { + if ((firstName || lastName) && !hasAutoDownloaded) { + const timer = setTimeout(() => { + handleSaveContact(); + setHasAutoDownloaded(true); + }, 500); + return () => clearTimeout(timer); + } + }, [firstName, lastName, hasAutoDownloaded]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleSaveContact = () => { + // Generate vCard format + const vCard = `BEGIN:VCARD +VERSION:3.0 +FN:${firstName} ${lastName} +N:${lastName};${firstName};;; +${organization ? `ORG:${organization}` : ''} +${title ? `TITLE:${title}` : ''} +${email ? `EMAIL:${email}` : ''} +${phone ? `TEL:${phone}` : ''} +END:VCARD`; + + // Create a blob and download + const blob = new Blob([vCard], { type: 'text/vcard;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${firstName}_${lastName}.vcf`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + // Show loading or error state + if (isLoading) { + return ( + <div style={{ + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + backgroundColor: '#ffffff' + }}> + <div style={{ textAlign: 'center' }}> + <div style={{ fontSize: '48px', marginBottom: '16px' }}>đŸ‘€</div> + <p style={{ color: '#666' }}>Loading contact...</p> + </div> + </div> + ); + } + + if (!firstName && !lastName) { + return ( + <div style={{ + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + padding: '20px', + backgroundColor: '#ffffff' + }}> + <div style={{ + maxWidth: '420px', + width: '100%', + padding: '48px 32px', + textAlign: 'center' + }}> + <div style={{ + fontSize: '64px', + marginBottom: '24px', + opacity: 0.3 + }}>đŸ‘€</div> + <h1 style={{ + fontSize: '22px', + fontWeight: '600', + marginBottom: '12px', + color: '#1a1a1a' + }}>No Contact Found</h1> + <p style={{ + color: '#666', + fontSize: '15px', + lineHeight: '1.6' + }}>This QR code doesn't contain any contact information.</p> + </div> + </div> + ); + } + + return ( + <div style={{ + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + padding: '20px', + backgroundColor: '#ffffff' + }}> + <div style={{ + maxWidth: '420px', + width: '100%', + padding: '48px 32px' + }}> + {/* Header */} + <div style={{ textAlign: 'center', marginBottom: '32px' }}> + <div style={{ + width: '80px', + height: '80px', + margin: '0 auto 20px', + borderRadius: '50%', + backgroundColor: '#f0f0f0', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '36px' + }}>đŸ‘€</div> + + <h1 style={{ + fontSize: '28px', + fontWeight: '600', + marginBottom: '8px', + color: '#1a1a1a', + letterSpacing: '-0.02em' + }}> + {firstName} {lastName} + </h1> + + {(title || organization) && ( + <p style={{ + color: '#666', + fontSize: '16px', + marginTop: '4px' + }}> + {title && organization && `${title} at ${organization}`} + {title && !organization && title} + {!title && organization && organization} + </p> + )} + </div> + + {/* Contact Details */} + <div style={{ marginBottom: '32px' }}> + {email && ( + <div style={{ + padding: '16px 20px', + backgroundColor: '#f8f8f8', + borderRadius: '12px', + marginBottom: '12px', + border: '1px solid #f0f0f0' + }}> + <div style={{ + fontSize: '11px', + color: '#888', + marginBottom: '6px', + textTransform: 'uppercase', + letterSpacing: '0.5px', + fontWeight: '500' + }}>Email</div> + <a href={`mailto:${email}`} style={{ + fontSize: '15px', + fontWeight: '500', + color: '#2563eb', + textDecoration: 'none', + wordBreak: 'break-all' + }}>{email}</a> + </div> + )} + + {phone && ( + <div style={{ + padding: '16px 20px', + backgroundColor: '#f8f8f8', + borderRadius: '12px', + border: '1px solid #f0f0f0' + }}> + <div style={{ + fontSize: '11px', + color: '#888', + marginBottom: '6px', + textTransform: 'uppercase', + letterSpacing: '0.5px', + fontWeight: '500' + }}>Phone</div> + <a href={`tel:${phone}`} style={{ + fontSize: '15px', + fontWeight: '500', + color: '#2563eb', + textDecoration: 'none' + }}>{phone}</a> + </div> + )} + </div> + + {/* Save Button */} + <button + onClick={handleSaveContact} + style={{ + width: '100%', + padding: '16px', + fontSize: '16px', + fontWeight: '600', + color: 'white', + backgroundColor: '#2563eb', + border: 'none', + borderRadius: '12px', + cursor: 'pointer', + transition: 'all 0.2s', + boxShadow: '0 2px 8px rgba(37, 99, 235, 0.2)' + }} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = '#1d4ed8'; + e.currentTarget.style.transform = 'translateY(-1px)'; + e.currentTarget.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = '#2563eb'; + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 2px 8px rgba(37, 99, 235, 0.2)'; + }} + > + Save to Contacts + </button> + + <p style={{ + textAlign: 'center', + fontSize: '13px', + color: '#999', + marginTop: '16px', + lineHeight: '1.5' + }}> + Add this contact to your address book + </p> + </div> + </div> + ); +} diff --git a/src/components/PostHogProvider.tsx b/src/components/PostHogProvider.tsx index 560071b..be52722 100644 --- a/src/components/PostHogProvider.tsx +++ b/src/components/PostHogProvider.tsx @@ -1,104 +1,104 @@ -'use client'; - -import { useEffect, useState, useRef } from 'react'; -import { usePathname, useSearchParams } from 'next/navigation'; -import posthog from 'posthog-js'; - -export function PostHogProvider({ children }: { children: React.ReactNode }) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const [isInitialized, setIsInitialized] = useState(false); - const initializationAttempted = useRef(false); - - // Initialize PostHog once - useEffect(() => { - // Prevent double initialization in React Strict Mode - if (initializationAttempted.current) return; - initializationAttempted.current = true; - - const cookieConsent = localStorage.getItem('cookieConsent'); - - if (cookieConsent === 'accepted') { - const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; - const apiHost = process.env.NEXT_PUBLIC_POSTHOG_HOST; - - if (!apiKey) { - console.warn('PostHog API key not configured'); - return; - } - - // Check if already initialized (using _loaded property) - if (!(posthog as any)._loaded) { - posthog.init(apiKey, { - api_host: apiHost || 'https://us.i.posthog.com', - person_profiles: 'identified_only', - capture_pageview: false, // Manual pageview tracking - capture_pageleave: true, - autocapture: true, - respect_dnt: true, - opt_out_capturing_by_default: false, - }); - - // Enable debug mode in development - if (process.env.NODE_ENV === 'development') { - posthog.debug(); - } - - // Set initialized immediately after init - setIsInitialized(true); - } else { - setIsInitialized(true); // Already loaded - } - } - - // NO cleanup function - PostHog should persist across page navigation - }, []); - - // Track page views ONLY after PostHog is initialized - useEffect(() => { - const cookieConsent = localStorage.getItem('cookieConsent'); - - if (cookieConsent === 'accepted' && pathname && isInitialized) { - let url = window.origin + pathname; - if (searchParams && searchParams.toString()) { - url = url + `?${searchParams.toString()}`; - } - - posthog.capture('$pageview', { - $current_url: url, - }); - } - }, [pathname, searchParams, isInitialized]); // Added isInitialized dependency - - return <>{children}</>; -} - -/** - * Helper function to identify user after login - */ -export function identifyUser(userId: string, traits?: Record<string, any>) { - const cookieConsent = localStorage.getItem('cookieConsent'); - if (cookieConsent === 'accepted' && (posthog as any)._loaded) { - posthog.identify(userId, traits); - } -} - -/** - * Helper function to track custom events - */ -export function trackEvent(eventName: string, properties?: Record<string, any>) { - const cookieConsent = localStorage.getItem('cookieConsent'); - if (cookieConsent === 'accepted' && (posthog as any)._loaded) { - posthog.capture(eventName, properties); - } -} - -/** - * Helper function to reset user on logout - */ -export function resetUser() { - const cookieConsent = localStorage.getItem('cookieConsent'); - if (cookieConsent === 'accepted' && (posthog as any)._loaded) { - posthog.reset(); - } -} +'use client'; + +import { useEffect, useState, useRef } from 'react'; +import { usePathname, useSearchParams } from 'next/navigation'; +import posthog from 'posthog-js'; + +export function PostHogProvider({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [isInitialized, setIsInitialized] = useState(false); + const initializationAttempted = useRef(false); + + // Initialize PostHog once + useEffect(() => { + // Prevent double initialization in React Strict Mode + if (initializationAttempted.current) return; + initializationAttempted.current = true; + + const cookieConsent = localStorage.getItem('cookieConsent'); + + if (cookieConsent === 'accepted') { + const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; + const apiHost = process.env.NEXT_PUBLIC_POSTHOG_HOST; + + if (!apiKey) { + console.warn('PostHog API key not configured'); + return; + } + + // Check if already initialized (using _loaded property) + if (!(posthog as any)._loaded) { + posthog.init(apiKey, { + api_host: apiHost || 'https://us.i.posthog.com', + person_profiles: 'identified_only', + capture_pageview: false, // Manual pageview tracking + capture_pageleave: true, + autocapture: true, + respect_dnt: true, + opt_out_capturing_by_default: false, + }); + + // Enable debug mode in development + if (process.env.NODE_ENV === 'development') { + posthog.debug(); + } + + // Set initialized immediately after init + setIsInitialized(true); + } else { + setIsInitialized(true); // Already loaded + } + } + + // NO cleanup function - PostHog should persist across page navigation + }, []); + + // Track page views ONLY after PostHog is initialized + useEffect(() => { + const cookieConsent = localStorage.getItem('cookieConsent'); + + if (cookieConsent === 'accepted' && pathname && isInitialized) { + let url = window.origin + pathname; + if (searchParams && searchParams.toString()) { + url = url + `?${searchParams.toString()}`; + } + + posthog.capture('$pageview', { + $current_url: url, + }); + } + }, [pathname, searchParams, isInitialized]); // Added isInitialized dependency + + return <>{children}</>; +} + +/** + * Helper function to identify user after login + */ +export function identifyUser(userId: string, traits?: Record<string, any>) { + const cookieConsent = localStorage.getItem('cookieConsent'); + if (cookieConsent === 'accepted' && (posthog as any)._loaded) { + posthog.identify(userId, traits); + } +} + +/** + * Helper function to track custom events + */ +export function trackEvent(eventName: string, properties?: Record<string, any>) { + const cookieConsent = localStorage.getItem('cookieConsent'); + if (cookieConsent === 'accepted' && (posthog as any)._loaded) { + posthog.capture(eventName, properties); + } +} + +/** + * Helper function to reset user on logout + */ +export function resetUser() { + const cookieConsent = localStorage.getItem('cookieConsent'); + if (cookieConsent === 'accepted' && (posthog as any)._loaded) { + posthog.reset(); + } +} diff --git a/src/components/SeoJsonLd.tsx b/src/components/SeoJsonLd.tsx index cceeb1c..5ab60fc 100644 --- a/src/components/SeoJsonLd.tsx +++ b/src/components/SeoJsonLd.tsx @@ -1,23 +1,23 @@ -import React from 'react'; - -interface SeoJsonLdProps { - data: object | object[]; -} - -export default function SeoJsonLd({ data }: SeoJsonLdProps) { - const jsonLdArray = Array.isArray(data) ? data : [data]; - - return ( - <> - {jsonLdArray.map((item, index) => ( - <script - key={index} - type="application/ld+json" - dangerouslySetInnerHTML={{ - __html: JSON.stringify(item, null, 0), - }} - /> - ))} - </> - ); -} +import React from 'react'; + +interface SeoJsonLdProps { + data: object | object[]; +} + +export default function SeoJsonLd({ data }: SeoJsonLdProps) { + const jsonLdArray = Array.isArray(data) ? data : [data]; + + return ( + <> + {jsonLdArray.map((item, index) => ( + <script + key={index} + type="application/ld+json" + dangerouslySetInnerHTML={{ + __html: JSON.stringify(item, null, 0), + }} + /> + ))} + </> + ); +} diff --git a/src/components/marketing/AIComingSoonBanner.tsx b/src/components/marketing/AIComingSoonBanner.tsx index 0a9292e..2d8c56c 100644 --- a/src/components/marketing/AIComingSoonBanner.tsx +++ b/src/components/marketing/AIComingSoonBanner.tsx @@ -1,216 +1,216 @@ -'use client'; - -import React, { useState } from 'react'; -import { Sparkles, Brain, TrendingUp, MessageSquare, Palette, ArrowRight, Mail, CheckCircle2, Lock } from 'lucide-react'; -import Link from 'next/link'; -import { motion } from 'framer-motion'; - -const AIComingSoonBanner = () => { - const [email, setEmail] = useState(''); - const [submitted, setSubmitted] = useState(false); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(''); - - try { - const response = await fetch('/api/newsletter/subscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email }), - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || 'Failed to subscribe'); - } - - setSubmitted(true); - setEmail(''); - } catch (err) { - setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.'); - } finally { - setLoading(false); - } - }; - - const features = [ - { - icon: Brain, - category: 'Smart QR Generation', - items: [ - 'AI-powered content optimization', - 'Intelligent design suggestions', - 'Auto-generate vCard from LinkedIn', - 'Smart URL shortening with SEO', - ], - }, - { - icon: TrendingUp, - category: 'Advanced Analytics & Insights', - items: [ - 'AI-powered scan predictions', - 'Anomaly detection', - 'Natural language analytics queries', - 'Automated insights reports', - ], - }, - { - icon: MessageSquare, - category: 'Smart Content Management', - items: [ - 'AI chatbot for instant support', - 'Automated QR categorization', - 'Smart bulk QR generation', - 'Content recommendations', - ], - }, - { - icon: Palette, - category: 'Creative & Marketing', - items: [ - 'AI-generated custom QR designs', - 'Marketing copy generation', - 'A/B testing suggestions', - 'Campaign optimization', - ], - }, - ]; - - return ( - <section className="relative overflow-hidden pt-12 pb-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-blue-50 via-white to-purple-50"> - {/* Animated Background Orbs (matching Hero) */} - <div className="absolute inset-0 overflow-hidden pointer-events-none"> - <div className="absolute top-0 left-1/4 w-96 h-96 bg-blue-400/20 rounded-full blur-3xl animate-blob" /> - <div className="absolute top-1/2 right-1/4 w-80 h-80 bg-purple-400/20 rounded-full blur-3xl animate-blob animation-delay-2000" /> - <div className="absolute bottom-0 left-1/2 w-96 h-96 bg-cyan-400/15 rounded-full blur-3xl animate-blob animation-delay-4000" /> - </div> - - <div className="max-w-6xl mx-auto relative z-10"> - {/* Header */} - {/* Header */} - <motion.div - initial={{ opacity: 0, scale: 0.95 }} - whileInView={{ opacity: 1, scale: 1 }} - viewport={{ once: true }} - transition={{ duration: 0.5 }} - className="text-center mb-12" - > - <div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-blue-100 mb-4 animate-pulse"> - <Sparkles className="w-4 h-4 text-blue-600" /> - <span className="text-sm font-medium text-blue-700"> - Coming Soon - </span> - </div> - - <h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-3"> - The Future of QR Codes is{' '} - <span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> - AI-Powered - </span> - </h2> - - <p className="text-gray-600 text-lg max-w-2xl mx-auto"> - Revolutionary AI features to transform how you create, manage, and optimize QR codes - </p> - </motion.div> - - {/* Features Grid */} - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12"> - {features.map((feature, index) => ( - <motion.div - key={index} - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: index * 0.1 }} - className="bg-white/80 backdrop-blur rounded-xl p-6 border border-gray-100 hover:shadow-lg transition-all hover:scale-105" - > - <div className="w-12 h-12 bg-gradient-to-br from-blue-100 to-purple-100 rounded-lg flex items-center justify-center mb-4"> - <feature.icon className="w-6 h-6 text-blue-600" /> - </div> - - <h3 className="font-semibold text-gray-900 mb-3"> - {feature.category} - </h3> - - <ul className="space-y-2"> - {feature.items.map((item, itemIndex) => ( - <li key={itemIndex} className="flex items-start gap-2 text-sm text-gray-600"> - <div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-blue-500 flex-shrink-0" /> - <span>{item}</span> - </li> - ))} - </ul> - </motion.div> - ))} - </div> - - {/* Email Capture */} - <motion.div - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: 0.4 }} - className="max-w-2xl mx-auto bg-gradient-to-br from-blue-50 to-purple-50 rounded-2xl p-8 border border-gray-100" - > - {!submitted ? ( - <> - <form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 mb-3"> - <div className="flex-1 relative"> - <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none" /> - <input - type="email" - value={email} - onChange={(e) => { - setEmail(e.target.value); - setError(''); - }} - placeholder="your@email.com" - required - disabled={loading} - className="w-full pl-12 pr-4 py-3 rounded-xl bg-white border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 outline-none transition-all" - /> - </div> - <button - type="submit" - disabled={loading} - className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold rounded-xl transition-all disabled:opacity-50 whitespace-nowrap flex items-center justify-center gap-2 group" - > - {loading ? 'Subscribing...' : ( - <> - Notify Me - <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" /> - </> - )} - </button> - </form> - {error && ( - <p className="text-sm text-red-600 mb-2">{error}</p> - )} - <p className="text-xs text-gray-500 text-center"> - Be the first to know when AI features launch - </p> - </> - ) : ( - <div className="flex items-center justify-center gap-2 text-green-600"> - <CheckCircle2 className="w-5 h-5" /> - <span className="font-medium"> - You're on the list! We'll notify you when AI features launch. - </span> - </div> - )} - </motion.div> - </div> - <div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-white pointer-events-none" /> - </section> - ); -}; - -export default AIComingSoonBanner; +'use client'; + +import React, { useState } from 'react'; +import { Sparkles, Brain, TrendingUp, MessageSquare, Palette, ArrowRight, Mail, CheckCircle2, Lock } from 'lucide-react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; + +const AIComingSoonBanner = () => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await fetch('/api/newsletter/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to subscribe'); + } + + setSubmitted(true); + setEmail(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.'); + } finally { + setLoading(false); + } + }; + + const features = [ + { + icon: Brain, + category: 'Smart QR Generation', + items: [ + 'AI-powered content optimization', + 'Intelligent design suggestions', + 'Auto-generate vCard from LinkedIn', + 'Smart URL shortening with SEO', + ], + }, + { + icon: TrendingUp, + category: 'Advanced Analytics & Insights', + items: [ + 'AI-powered scan predictions', + 'Anomaly detection', + 'Natural language analytics queries', + 'Automated insights reports', + ], + }, + { + icon: MessageSquare, + category: 'Smart Content Management', + items: [ + 'AI chatbot for instant support', + 'Automated QR categorization', + 'Smart bulk QR generation', + 'Content recommendations', + ], + }, + { + icon: Palette, + category: 'Creative & Marketing', + items: [ + 'AI-generated custom QR designs', + 'Marketing copy generation', + 'A/B testing suggestions', + 'Campaign optimization', + ], + }, + ]; + + return ( + <section className="relative overflow-hidden pt-12 pb-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-blue-50 via-white to-purple-50"> + {/* Animated Background Orbs (matching Hero) */} + <div className="absolute inset-0 overflow-hidden pointer-events-none"> + <div className="absolute top-0 left-1/4 w-96 h-96 bg-blue-400/20 rounded-full blur-3xl animate-blob" /> + <div className="absolute top-1/2 right-1/4 w-80 h-80 bg-purple-400/20 rounded-full blur-3xl animate-blob animation-delay-2000" /> + <div className="absolute bottom-0 left-1/2 w-96 h-96 bg-cyan-400/15 rounded-full blur-3xl animate-blob animation-delay-4000" /> + </div> + + <div className="max-w-6xl mx-auto relative z-10"> + {/* Header */} + {/* Header */} + <motion.div + initial={{ opacity: 0, scale: 0.95 }} + whileInView={{ opacity: 1, scale: 1 }} + viewport={{ once: true }} + transition={{ duration: 0.5 }} + className="text-center mb-12" + > + <div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-blue-100 mb-4 animate-pulse"> + <Sparkles className="w-4 h-4 text-blue-600" /> + <span className="text-sm font-medium text-blue-700"> + Coming Soon + </span> + </div> + + <h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-3"> + The Future of QR Codes is{' '} + <span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> + AI-Powered + </span> + </h2> + + <p className="text-gray-600 text-lg max-w-2xl mx-auto"> + Revolutionary AI features to transform how you create, manage, and optimize QR codes + </p> + </motion.div> + + {/* Features Grid */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12"> + {features.map((feature, index) => ( + <motion.div + key={index} + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: index * 0.1 }} + className="bg-white/80 backdrop-blur rounded-xl p-6 border border-gray-100 hover:shadow-lg transition-all hover:scale-105" + > + <div className="w-12 h-12 bg-gradient-to-br from-blue-100 to-purple-100 rounded-lg flex items-center justify-center mb-4"> + <feature.icon className="w-6 h-6 text-blue-600" /> + </div> + + <h3 className="font-semibold text-gray-900 mb-3"> + {feature.category} + </h3> + + <ul className="space-y-2"> + {feature.items.map((item, itemIndex) => ( + <li key={itemIndex} className="flex items-start gap-2 text-sm text-gray-600"> + <div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-blue-500 flex-shrink-0" /> + <span>{item}</span> + </li> + ))} + </ul> + </motion.div> + ))} + </div> + + {/* Email Capture */} + <motion.div + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: 0.4 }} + className="max-w-2xl mx-auto bg-gradient-to-br from-blue-50 to-purple-50 rounded-2xl p-8 border border-gray-100" + > + {!submitted ? ( + <> + <form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 mb-3"> + <div className="flex-1 relative"> + <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none" /> + <input + type="email" + value={email} + onChange={(e) => { + setEmail(e.target.value); + setError(''); + }} + placeholder="your@email.com" + required + disabled={loading} + className="w-full pl-12 pr-4 py-3 rounded-xl bg-white border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 outline-none transition-all" + /> + </div> + <button + type="submit" + disabled={loading} + className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold rounded-xl transition-all disabled:opacity-50 whitespace-nowrap flex items-center justify-center gap-2 group" + > + {loading ? 'Subscribing...' : ( + <> + Notify Me + <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" /> + </> + )} + </button> + </form> + {error && ( + <p className="text-sm text-red-600 mb-2">{error}</p> + )} + <p className="text-xs text-gray-500 text-center"> + Be the first to know when AI features launch + </p> + </> + ) : ( + <div className="flex items-center justify-center gap-2 text-green-600"> + <CheckCircle2 className="w-5 h-5" /> + <span className="font-medium"> + You're on the list! We'll notify you when AI features launch. + </span> + </div> + )} + </motion.div> + </div> + <div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-white pointer-events-none" /> + </section> + ); +}; + +export default AIComingSoonBanner; diff --git a/src/components/marketing/FAQ.tsx b/src/components/marketing/FAQ.tsx index 3fa7b6b..65454b6 100644 --- a/src/components/marketing/FAQ.tsx +++ b/src/components/marketing/FAQ.tsx @@ -1,92 +1,92 @@ -'use client'; - -import React, { useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Card } from '@/components/ui/Card'; - -interface FAQProps { - t: any; // i18n translation function -} - -export const FAQ: React.FC<FAQProps> = ({ t }) => { - const [openIndex, setOpenIndex] = useState<number | null>(null); - - const questions = [ - 'account', - 'static_vs_dynamic', - 'forever', - 'file_type', - 'analytics', - ]; - - return ( - <section id="faq" className="py-16 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <motion.div - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5 }} - className="text-center mb-12" - > - <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> - {t.faq.title} - </h2> - </motion.div> - - <div className="max-w-3xl mx-auto space-y-4"> - {questions.map((key, index) => ( - <motion.div - key={key} - initial={{ opacity: 0, x: -20 }} - whileInView={{ opacity: 1, x: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: index * 0.1 }} - > - <Card className="cursor-pointer border-gray-200 hover:border-gray-300 transition-colors" onClick={() => setOpenIndex(openIndex === index ? null : index)}> - <div className="p-6"> - <div className="flex items-center justify-between"> - <h3 className="text-lg font-semibold text-gray-900"> - {t.faq.questions[key].question} - </h3> - <svg - className={`w-5 h-5 text-gray-500 transition-transform duration-300 ${openIndex === index ? 'rotate-180' : ''}`} - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - aria-hidden="true" - > - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> - </svg> - </div> - - <AnimatePresence> - {openIndex === index && ( - <motion.div - initial={{ height: 0, opacity: 0, marginTop: 0 }} - animate={{ height: 'auto', opacity: 1, marginTop: 16 }} - exit={{ height: 0, opacity: 0, marginTop: 0 }} - transition={{ duration: 0.3 }} - className="overflow-hidden" - > - <div className="text-gray-600"> - {t.faq.questions[key].answer} - </div> - </motion.div> - )} - </AnimatePresence> - </div> - </Card> - </motion.div> - ))} - </div> - - <div className="text-center mt-8"> - <a href="/faq" className="text-primary-600 hover:text-primary-700 font-medium"> - View All Questions → - </a> - </div> - </div> - </section> - ); +'use client'; + +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '@/components/ui/Card'; + +interface FAQProps { + t: any; // i18n translation function +} + +export const FAQ: React.FC<FAQProps> = ({ t }) => { + const [openIndex, setOpenIndex] = useState<number | null>(null); + + const questions = [ + 'account', + 'static_vs_dynamic', + 'forever', + 'file_type', + 'analytics', + ]; + + return ( + <section id="faq" className="py-16 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <motion.div + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5 }} + className="text-center mb-12" + > + <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> + {t.faq.title} + </h2> + </motion.div> + + <div className="max-w-3xl mx-auto space-y-4"> + {questions.map((key, index) => ( + <motion.div + key={key} + initial={{ opacity: 0, x: -20 }} + whileInView={{ opacity: 1, x: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: index * 0.1 }} + > + <Card className="cursor-pointer border-gray-200 hover:border-gray-300 transition-colors" onClick={() => setOpenIndex(openIndex === index ? null : index)}> + <div className="p-6"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-semibold text-gray-900"> + {t.faq.questions[key].question} + </h3> + <svg + className={`w-5 h-5 text-gray-500 transition-transform duration-300 ${openIndex === index ? 'rotate-180' : ''}`} + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + aria-hidden="true" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> + </svg> + </div> + + <AnimatePresence> + {openIndex === index && ( + <motion.div + initial={{ height: 0, opacity: 0, marginTop: 0 }} + animate={{ height: 'auto', opacity: 1, marginTop: 16 }} + exit={{ height: 0, opacity: 0, marginTop: 0 }} + transition={{ duration: 0.3 }} + className="overflow-hidden" + > + <div className="text-gray-600"> + {t.faq.questions[key].answer} + </div> + </motion.div> + )} + </AnimatePresence> + </div> + </Card> + </motion.div> + ))} + </div> + + <div className="text-center mt-8"> + <a href="/faq" className="text-primary-600 hover:text-primary-700 font-medium"> + View All Questions → + </a> + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/components/marketing/Features.tsx b/src/components/marketing/Features.tsx index 0a7b532..c40ae46 100644 --- a/src/components/marketing/Features.tsx +++ b/src/components/marketing/Features.tsx @@ -1,85 +1,85 @@ -'use client'; - -import React from 'react'; -import { motion } from 'framer-motion'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; - -interface FeaturesProps { - t: any; // i18n translation function -} - -export const Features: React.FC<FeaturesProps> = ({ t }) => { - const features = [ - { - key: 'analytics', - icon: ( - <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> - </svg> - ), - color: 'text-blue-600 bg-blue-100', - }, - { - key: 'customization', - icon: ( - <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /> - </svg> - ), - color: 'text-purple-600 bg-purple-100', - }, - { - key: 'unlimited', - icon: ( - <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> - </svg> - ), - color: 'text-green-600 bg-green-100', - }, - ]; - - return ( - <section className="py-16 bg-gray-50"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <motion.div - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5 }} - className="text-center mb-12" - > - <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> - {t.features.title} - </h2> - </motion.div> - - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto"> - {features.map((feature, index) => ( - <motion.div - key={feature.key} - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: index * 0.1 }} - > - <Card hover className="h-full border-gray-100 hover:border-primary-100 hover:shadow-lg transition-all"> - <CardHeader> - <div className={`w-12 h-12 rounded-lg ${feature.color} flex items-center justify-center mb-4`}> - {feature.icon} - </div> - <CardTitle>{t.features[feature.key].title}</CardTitle> - </CardHeader> - <CardContent> - <p className="text-gray-600"> - {t.features[feature.key].description} - </p> - </CardContent> - </Card> - </motion.div> - ))} - </div> - </div> - </section> - ); +'use client'; + +import React from 'react'; +import { motion } from 'framer-motion'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; + +interface FeaturesProps { + t: any; // i18n translation function +} + +export const Features: React.FC<FeaturesProps> = ({ t }) => { + const features = [ + { + key: 'analytics', + icon: ( + <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> + </svg> + ), + color: 'text-blue-600 bg-blue-100', + }, + { + key: 'customization', + icon: ( + <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /> + </svg> + ), + color: 'text-purple-600 bg-purple-100', + }, + { + key: 'unlimited', + icon: ( + <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> + ), + color: 'text-green-600 bg-green-100', + }, + ]; + + return ( + <section className="py-16 bg-gray-50"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <motion.div + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5 }} + className="text-center mb-12" + > + <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> + {t.features.title} + </h2> + </motion.div> + + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto"> + {features.map((feature, index) => ( + <motion.div + key={feature.key} + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: index * 0.1 }} + > + <Card hover className="h-full border-gray-100 hover:border-primary-100 hover:shadow-lg transition-all"> + <CardHeader> + <div className={`w-12 h-12 rounded-lg ${feature.color} flex items-center justify-center mb-4`}> + {feature.icon} + </div> + <CardTitle>{t.features[feature.key].title}</CardTitle> + </CardHeader> + <CardContent> + <p className="text-gray-600"> + {t.features[feature.key].description} + </p> + </CardContent> + </Card> + </motion.div> + ))} + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/components/marketing/Hero.tsx b/src/components/marketing/Hero.tsx index b5bceb7..e9ae338 100644 --- a/src/components/marketing/Hero.tsx +++ b/src/components/marketing/Hero.tsx @@ -1,155 +1,225 @@ -'use client'; - -import React from 'react'; -import Link from 'next/link'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { Card } from '@/components/ui/Card'; -import { motion } from 'framer-motion'; -import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight } from 'lucide-react'; - -interface HeroProps { - t: any; // i18n translation function -} - -export const Hero: React.FC<HeroProps> = ({ t }) => { - const templateCards = [ - { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe }, - { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User }, - { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin }, - { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone }, - ]; - - const containerjs = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.1 - } - } - }; - - const itemjs = { - hidden: { opacity: 0, y: 20 }, - show: { opacity: 1, y: 0 } - }; - - return ( - <section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20"> - {/* Animated Background Orbs */} - <div className="absolute inset-0 overflow-hidden pointer-events-none"> - {/* Orb 1 - Blue (top-left) */} - <div className="absolute -top-24 -left-24 w-96 h-96 bg-blue-400/30 rounded-full blur-3xl animate-blob" /> - - {/* Orb 2 - Purple (top-right) */} - <div className="absolute -top-12 -right-12 w-96 h-96 bg-purple-400/30 rounded-full blur-3xl animate-blob animation-delay-2000" /> - - {/* Orb 3 - Pink (bottom-left) */} - <div className="absolute -bottom-24 -left-12 w-96 h-96 bg-pink-400/20 rounded-full blur-3xl animate-blob animation-delay-4000" /> - - {/* Orb 4 - Cyan (center-right) */} - <div className="absolute top-1/2 -right-24 w-80 h-80 bg-cyan-400/20 rounded-full blur-3xl animate-blob animation-delay-6000" /> - </div> - - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10"> - <div className="grid lg:grid-cols-2 gap-12 items-center"> - {/* Left Content */} - <div className="space-y-8"> - <Badge variant="info" className="inline-flex items-center space-x-2"> - <span>{t.hero.badge}</span> - </Badge> - - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.5 }} - className="space-y-6" - > - <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> - {t.hero.title} - </h1> - - <p className="text-xl text-gray-600 leading-relaxed max-w-2xl"> - {t.hero.subtitle} - </p> - - <div className="space-y-3 pt-2"> - {t.hero.features.map((feature: string, index: number) => ( - <motion.div - key={index} - initial={{ opacity: 0, x: -20 }} - animate={{ opacity: 1, x: 0 }} - transition={{ delay: 0.2 + (index * 0.1) }} - className="flex items-center space-x-3" - > - <div className="flex-shrink-0 w-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center"> - <CheckCircle2 className="w-4 h-4 text-emerald-600" /> - </div> - <span className="text-gray-700 font-medium">{feature}</span> - </motion.div> - ))} - </div> - </motion.div> - - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.5 }} - className="flex flex-col sm:flex-row gap-4 pt-4" - > - <Link href="/signup"> - <Button size="lg" className="text-lg px-8 py-6 w-full sm:w-auto shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40 transition-all duration-300"> - {t.hero.cta_primary} - </Button> - </Link> - <Link href="/#pricing"> - <Button variant="outline" size="lg" className="text-lg px-8 py-6 w-full sm:w-auto backdrop-blur-sm bg-white/50 border-gray-200 hover:bg-white/80 transition-all duration-300"> - {t.hero.cta_secondary} - </Button> - </Link> - </motion.div> - </div> - - {/* Right Preview Widget */} - <div className="relative"> - <motion.div - variants={containerjs} - initial="hidden" - animate="show" - className="grid grid-cols-2 gap-4" - > - {templateCards.map((card, index) => ( - <motion.div key={index} variants={itemjs}> - <Card className={`backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-6 text-center hover:scale-105 transition-all duration-300 group cursor-pointer`}> - <div className={`w-12 h-12 mx-auto mb-4 rounded-xl ${card.color} flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}> - <card.icon className="w-6 h-6" /> - </div> - <p className="font-semibold text-gray-800 group-hover:text-gray-900">{card.title}</p> - </Card> - </motion.div> - ))} - </motion.div> - - {/* Floating Badge */} - <motion.div - initial={{ opacity: 0, scale: 0.8 }} - animate={{ opacity: 1, scale: 1 }} - transition={{ delay: 0.8 }} - className="absolute -top-4 -right-4 bg-gradient-to-r from-success-500 to-emerald-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg shadow-success-500/30 flex items-center gap-2" - > - <span className="relative flex h-2 w-2"> - <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span> - <span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span> - </span> - {t.hero.engagement_badge} - </motion.div> - </div> - </div> - </div> - - {/* Smooth Gradient Fade Transition */} - <div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" /> - </section > - ); +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; +import { Card } from '@/components/ui/Card'; +import { motion } from 'framer-motion'; +import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight, FileText, Ticket, Smartphone, Star } from 'lucide-react'; +import { useState, useEffect } from 'react'; + +// Sub-component for the flipping effect +// Sub-component for the flipping effect +const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => { + const [isFlipped, setIsFlipped] = useState(false); + + useEffect(() => { + // Initial delay + const initialTimeout = setTimeout(() => { + setIsFlipped(true); // First flip + + // Setup interval for subsequent flips + const interval = setInterval(() => { + setIsFlipped(prev => !prev); + }, 8000); // Toggle every 8 seconds to prevent overlap (4 cards * 2s gap) + + return () => clearInterval(interval); + }, delay * 1000); + + return () => clearTimeout(initialTimeout); + }, [delay]); + + return ( + <div className="relative h-32 w-full perspective-[1000px] group cursor-pointer"> + <motion.div + animate={{ rotateY: isFlipped ? 180 : 0 }} + transition={{ duration: 0.6, type: "spring", stiffness: 260, damping: 20 }} + className="relative w-full h-full preserve-3d" + style={{ transformStyle: 'preserve-3d' }} + > + {/* Front Face */} + <div + className="absolute inset-0 backface-hidden" + style={{ backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }} + > + <Card className="w-full h-full backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300"> + <div className={`w-10 h-10 mb-3 rounded-xl ${front.color} flex items-center justify-center`}> + <front.icon className="w-5 h-5" /> + </div> + <p className="font-semibold text-gray-800 text-sm">{front.title}</p> + </Card> + </div> + + {/* Back Face */} + <div + className="absolute inset-0 backface-hidden" + style={{ + backfaceVisibility: 'hidden', + WebkitBackfaceVisibility: 'hidden', + transform: 'rotateY(180deg)' + }} + > + <Card className="w-full h-full backdrop-blur-xl bg-white/80 border-white/60 shadow-xl shadow-blue-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300"> + <div className={`w-10 h-10 mb-3 rounded-xl ${back.color} flex items-center justify-center`}> + <back.icon className="w-5 h-5" /> + </div> + <p className="font-semibold text-gray-900 text-sm">{back.title}</p> + </Card> + </div> + </motion.div> + </div> + ); +}; + +interface HeroProps { + t: any; // i18n translation function +} + +export const Hero: React.FC<HeroProps> = ({ t }) => { + + + const containerjs = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.1 + } + } + }; + + const itemjs = { + hidden: { opacity: 0, y: 20 }, + show: { opacity: 1, y: 0 } + }; + + return ( + <section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-20"> + {/* Animated Background Orbs */} + <div className="absolute inset-0 overflow-hidden pointer-events-none"> + {/* Orb 1 - Blue (top-left) */} + <div className="absolute -top-24 -left-24 w-96 h-96 bg-blue-400/30 rounded-full blur-3xl animate-blob" /> + + {/* Orb 2 - Purple (top-right) */} + <div className="absolute -top-12 -right-12 w-96 h-96 bg-purple-400/30 rounded-full blur-3xl animate-blob animation-delay-2000" /> + + {/* Orb 3 - Pink (bottom-left) */} + <div className="absolute -bottom-24 -left-12 w-96 h-96 bg-pink-400/20 rounded-full blur-3xl animate-blob animation-delay-4000" /> + + {/* Orb 4 - Cyan (center-right) */} + <div className="absolute top-1/2 -right-24 w-80 h-80 bg-cyan-400/20 rounded-full blur-3xl animate-blob animation-delay-6000" /> + </div> + + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative z-10"> + <div className="grid lg:grid-cols-2 gap-12 items-center"> + {/* Left Content */} + <div className="space-y-8"> + <Badge variant="info" className="inline-flex items-center space-x-2"> + <span>{t.hero.badge}</span> + </Badge> + + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5 }} + className="space-y-6" + > + <h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight"> + {t.hero.title} + </h1> + + <p className="text-xl text-gray-600 leading-relaxed max-w-2xl"> + {t.hero.subtitle} + </p> + + <div className="space-y-3 pt-2"> + {t.hero.features.map((feature: string, index: number) => ( + <motion.div + key={index} + initial={{ opacity: 0, x: -20 }} + animate={{ opacity: 1, x: 0 }} + transition={{ delay: 0.2 + (index * 0.1) }} + className="flex items-center space-x-3" + > + <div className="flex-shrink-0 w-6 h-6 bg-emerald-100 rounded-full flex items-center justify-center"> + <CheckCircle2 className="w-4 h-4 text-emerald-600" /> + </div> + <span className="text-gray-700 font-medium">{feature}</span> + </motion.div> + ))} + </div> + </motion.div> + + <motion.div + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.5 }} + className="flex flex-col sm:flex-row gap-4 pt-4" + > + <Link href="/signup"> + <Button size="lg" className="text-lg px-8 py-6 w-full sm:w-auto shadow-lg shadow-blue-500/25 hover:shadow-blue-500/40 transition-all duration-300"> + {t.hero.cta_primary} + </Button> + </Link> + <Link href="/#pricing"> + <Button variant="outline" size="lg" className="text-lg px-8 py-6 w-full sm:w-auto backdrop-blur-sm bg-white/50 border-gray-200 hover:bg-white/80 transition-all duration-300"> + {t.hero.cta_secondary} + </Button> + </Link> + </motion.div> + </div> + + {/* Right Preview Widget */} + <div className="relative"> + <div className="relative perspective-[1000px]"> + <div className="grid grid-cols-2 gap-4"> + {[ + { + front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe }, + back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText }, + delay: 3 // Starts at 3s + }, + { + front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User }, + back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket }, + delay: 5 // +2s + }, + { + front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin }, + back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone }, + delay: 7 // +2s + }, + { + front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone }, + back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star }, + delay: 9 // +2s + }, + ].map((card, index) => ( + <FlippingCard key={index} {...card} /> + ))} + </div> + </div> + + {/* Floating Badge */} + <motion.div + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ delay: 0.8 }} + className="absolute -top-4 -right-4 bg-gradient-to-r from-success-500 to-emerald-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg shadow-success-500/30 flex items-center gap-2" + > + <span className="relative flex h-2 w-2"> + <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span> + <span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span> + </span> + {t.hero.engagement_badge} + </motion.div> + </div> + </div> + </div> + + {/* Smooth Gradient Fade Transition */} + <div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" /> + </section > + ); }; \ No newline at end of file diff --git a/src/components/marketing/InstantGenerator.tsx b/src/components/marketing/InstantGenerator.tsx index 82d72b7..d6f3a9a 100644 --- a/src/components/marketing/InstantGenerator.tsx +++ b/src/components/marketing/InstantGenerator.tsx @@ -1,282 +1,282 @@ -'use client'; - -import React, { useState } from 'react'; -import { QRCodeSVG } from 'qrcode.react'; -import { motion } from 'framer-motion'; -import { Card } from '@/components/ui/Card'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { calculateContrast } from '@/lib/utils'; - -interface InstantGeneratorProps { - t: any; // i18n translation function -} - -export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => { - const [url, setUrl] = useState('https://example.com'); - const [foregroundColor, setForegroundColor] = useState('#000000'); - const [backgroundColor, setBackgroundColor] = useState('#FFFFFF'); - const [cornerStyle, setCornerStyle] = useState('square'); - const [size, setSize] = useState(200); - - const contrast = calculateContrast(foregroundColor, backgroundColor); - const hasGoodContrast = contrast >= 4.5; - - const downloadQR = (format: 'svg' | 'png') => { - const svg = document.querySelector('#instant-qr-preview svg'); - if (!svg || !url) return; - - if (format === 'svg') { - const svgData = new XMLSerializer().serializeToString(svg); - const blob = new Blob([svgData], { type: 'image/svg+xml' }); - const downloadUrl = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = downloadUrl; - a.download = 'qrcode.svg'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(downloadUrl); - } else { - // Convert SVG to PNG using Canvas - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const img = new Image(); - const svgData = new XMLSerializer().serializeToString(svg); - const blob = new Blob([svgData], { type: 'image/svg+xml' }); - const url = URL.createObjectURL(blob); - - img.onload = () => { - canvas.width = size; - canvas.height = size; - if (ctx) { - ctx.fillStyle = backgroundColor; - ctx.fillRect(0, 0, size, size); - ctx.drawImage(img, 0, 0, size, size); - } - canvas.toBlob((blob) => { - if (blob) { - const downloadUrl = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = downloadUrl; - a.download = 'qrcode.png'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(downloadUrl); - } - }); - URL.revokeObjectURL(url); - }; - img.src = url; - } - }; - - return ( - <section className="pt-16 pb-32 bg-gray-50 border-t border-gray-100 relative"> - <div - className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-r from-blue-50 to-white pointer-events-none" - style={{ maskImage: 'linear-gradient(to bottom, transparent, black)', WebkitMaskImage: 'linear-gradient(to bottom, transparent, black)' }} - /> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <motion.div - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5 }} - className="text-center mb-12" - > - <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> - {t.generator.title} - </h2> - </motion.div> - - <div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto"> - {/* Left Form */} - <motion.div - initial={{ opacity: 0, x: -20 }} - whileInView={{ opacity: 1, x: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: 0.2 }} - > - <Card className="space-y-6 shadow-xl shadow-gray-200/50 border-gray-100"> - <Input - label="URL" - value={url} - onChange={(e) => setUrl(e.target.value)} - placeholder={t.generator.url_placeholder} - className="transition-all focus:ring-2 focus:ring-primary-500/20" - /> - - <div className="grid grid-cols-2 gap-4"> - <div> - <label htmlFor="foreground-color" className="block text-sm font-medium text-gray-700 mb-2"> - {t.generator.foreground} - </label> - <div className="flex items-center space-x-2"> - <input - id="foreground-color" - type="color" - value={foregroundColor} - onChange={(e) => setForegroundColor(e.target.value)} - className="w-14 h-12 rounded border border-gray-300 cursor-pointer" - aria-label="Foreground color picker" - /> - <Input - id="foreground-color-text" - value={foregroundColor} - onChange={(e) => setForegroundColor(e.target.value)} - className="flex-1" - aria-label="Foreground color hex value" - /> - </div> - </div> - - <div> - <label htmlFor="background-color" className="block text-sm font-medium text-gray-700 mb-2"> - {t.generator.background} - </label> - <div className="flex items-center space-x-2"> - <input - id="background-color" - type="color" - value={backgroundColor} - onChange={(e) => setBackgroundColor(e.target.value)} - className="w-14 h-12 rounded border border-gray-300 cursor-pointer" - aria-label="Background color picker" - /> - <Input - id="background-color-text" - value={backgroundColor} - onChange={(e) => setBackgroundColor(e.target.value)} - className="flex-1" - aria-label="Background color hex value" - /> - </div> - </div> - </div> - - <div className="grid grid-cols-2 gap-4"> - <div> - <label htmlFor="corner-style" className="block text-sm font-medium text-gray-700 mb-2"> - {t.generator.corners} - </label> - <select - id="corner-style" - value={cornerStyle} - onChange={(e) => setCornerStyle(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" - > - <option value="square">Square</option> - <option value="rounded">Rounded</option> - </select> - </div> - - <div> - <label htmlFor="qr-size" className="block text-sm font-medium text-gray-700 mb-2"> - {t.generator.size} - </label> - <input - id="qr-size" - type="range" - min="100" - max="400" - value={size} - onChange={(e) => setSize(Number(e.target.value))} - className="w-full accent-primary-600" - aria-label={`QR code size: ${size} pixels`} - /> - <div className="text-sm text-gray-500 text-center mt-1" aria-hidden="true">{size}px</div> - </div> - </div> - - <div className="flex items-center justify-between"> - <Badge variant={hasGoodContrast ? 'success' : 'warning'}> - {hasGoodContrast ? t.generator.contrast_good : 'Low contrast'} - </Badge> - <div className="text-sm text-gray-500"> - Contrast: {contrast.toFixed(1)}:1 - </div> - </div> - - <div className="flex space-x-3"> - <Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('svg')}> - {t.generator.download_svg} - </Button> - <Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('png')}> - {t.generator.download_png} - </Button> - </div> - - <Button className="w-full text-lg py-6 shadow-lg shadow-primary-500/20 hover:shadow-primary-500/40 transition-all" onClick={() => window.location.href = '/login'}> - {t.generator.save_track} - </Button> - </Card> - </motion.div> - - {/* Right Preview */} - <motion.div - initial={{ opacity: 0, x: 20 }} - whileInView={{ opacity: 1, x: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: 0.4 }} - className="flex flex-col items-center justify-center p-8 bg-gradient-to-br from-blue-50/50 to-purple-50/50 rounded-2xl border border-blue-100/50 shadow-lg shadow-blue-500/5 relative overflow-hidden backdrop-blur-sm" - > - {/* Artistic Curved Lines Background */} - <div className="absolute inset-0 opacity-[0.4]"> - <svg className="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none"> - <defs> - <linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%"> - <stop offset="0%" stopColor="#60a5fa" stopOpacity="0.4" /> - <stop offset="100%" stopColor="#c084fc" stopOpacity="0.4" /> - </linearGradient> - <linearGradient id="gradient2" x1="100%" y1="0%" x2="0%" y2="100%"> - <stop offset="0%" stopColor="#818cf8" stopOpacity="0.4" /> - <stop offset="100%" stopColor="#38bdf8" stopOpacity="0.4" /> - </linearGradient> - </defs> - <path d="M0 100 Q 25 30 50 70 T 100 0" fill="none" stroke="url(#gradient1)" strokeWidth="0.8" className="opacity-60" /> - <path d="M0 50 Q 40 80 70 30 T 100 50" fill="none" stroke="url(#gradient2)" strokeWidth="0.8" className="opacity-60" /> - <path d="M0 0 Q 30 60 60 20 T 100 80" fill="none" stroke="url(#gradient1)" strokeWidth="0.6" className="opacity-40" /> - </svg> - <div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-15 brightness-100 contrast-150 mix-blend-overlay"></div> - </div> - - {/* Decorative Orbs */} - <div className="absolute -top-20 -right-20 w-64 h-64 bg-purple-200/30 rounded-full blur-3xl animate-blob"></div> - <div className="absolute -bottom-20 -left-20 w-64 h-64 bg-blue-200/30 rounded-full blur-3xl animate-blob animation-delay-2000"></div> - - <div className="text-center w-full relative z-10"> - <h3 className="text-xl font-bold mb-8 text-gray-800">{t.generator.live_preview}</h3> - <div id="instant-qr-preview" className="flex justify-center mb-8 transform hover:scale-105 transition-transform duration-300"> - {url ? ( - <div className={`${cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''} p-4 bg-white shadow-lg rounded-xl`}> - <QRCodeSVG - value={url} - size={size} - fgColor={foregroundColor} - bgColor={backgroundColor} - level="M" - /> - </div> - ) : ( - <div - className="bg-gray-100 rounded-xl flex items-center justify-center text-gray-500 animate-pulse" - style={{ width: 200, height: 200 }} - > - Enter URL - </div> - )} - </div> - <div className="text-sm font-medium text-gray-600 mb-2 bg-gray-50 py-2 px-4 rounded-full inline-block"> - {url || 'https://example.com'} - </div> - <div className="text-xs text-gray-400 mt-2">{t.generator.demo_note}</div> - </div> - </motion.div> - </div> - </div> - </section> - ); +'use client'; + +import React, { useState } from 'react'; +import { QRCodeSVG } from 'qrcode.react'; +import { motion } from 'framer-motion'; +import { Card } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; +import { calculateContrast } from '@/lib/utils'; + +interface InstantGeneratorProps { + t: any; // i18n translation function +} + +export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => { + const [url, setUrl] = useState('https://example.com'); + const [foregroundColor, setForegroundColor] = useState('#000000'); + const [backgroundColor, setBackgroundColor] = useState('#FFFFFF'); + const [cornerStyle, setCornerStyle] = useState('square'); + const [size, setSize] = useState(200); + + const contrast = calculateContrast(foregroundColor, backgroundColor); + const hasGoodContrast = contrast >= 4.5; + + const downloadQR = (format: 'svg' | 'png') => { + const svg = document.querySelector('#instant-qr-preview svg'); + if (!svg || !url) return; + + if (format === 'svg') { + const svgData = new XMLSerializer().serializeToString(svg); + const blob = new Blob([svgData], { type: 'image/svg+xml' }); + const downloadUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = downloadUrl; + a.download = 'qrcode.svg'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(downloadUrl); + } else { + // Convert SVG to PNG using Canvas + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + const svgData = new XMLSerializer().serializeToString(svg); + const blob = new Blob([svgData], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + + img.onload = () => { + canvas.width = size; + canvas.height = size; + if (ctx) { + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, size, size); + ctx.drawImage(img, 0, 0, size, size); + } + canvas.toBlob((blob) => { + if (blob) { + const downloadUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = downloadUrl; + a.download = 'qrcode.png'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(downloadUrl); + } + }); + URL.revokeObjectURL(url); + }; + img.src = url; + } + }; + + return ( + <section className="pt-16 pb-32 bg-gray-50 border-t border-gray-100 relative"> + <div + className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-r from-blue-50 to-white pointer-events-none" + style={{ maskImage: 'linear-gradient(to bottom, transparent, black)', WebkitMaskImage: 'linear-gradient(to bottom, transparent, black)' }} + /> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <motion.div + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5 }} + className="text-center mb-12" + > + <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> + {t.generator.title} + </h2> + </motion.div> + + <div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto"> + {/* Left Form */} + <motion.div + initial={{ opacity: 0, x: -20 }} + whileInView={{ opacity: 1, x: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: 0.2 }} + > + <Card className="space-y-6 shadow-xl shadow-gray-200/50 border-gray-100"> + <Input + label="URL" + value={url} + onChange={(e) => setUrl(e.target.value)} + placeholder={t.generator.url_placeholder} + className="transition-all focus:ring-2 focus:ring-primary-500/20" + /> + + <div className="grid grid-cols-2 gap-4"> + <div> + <label htmlFor="foreground-color" className="block text-sm font-medium text-gray-700 mb-2"> + {t.generator.foreground} + </label> + <div className="flex items-center space-x-2"> + <input + id="foreground-color" + type="color" + value={foregroundColor} + onChange={(e) => setForegroundColor(e.target.value)} + className="w-14 h-12 rounded border border-gray-300 cursor-pointer" + aria-label="Foreground color picker" + /> + <Input + id="foreground-color-text" + value={foregroundColor} + onChange={(e) => setForegroundColor(e.target.value)} + className="flex-1" + aria-label="Foreground color hex value" + /> + </div> + </div> + + <div> + <label htmlFor="background-color" className="block text-sm font-medium text-gray-700 mb-2"> + {t.generator.background} + </label> + <div className="flex items-center space-x-2"> + <input + id="background-color" + type="color" + value={backgroundColor} + onChange={(e) => setBackgroundColor(e.target.value)} + className="w-14 h-12 rounded border border-gray-300 cursor-pointer" + aria-label="Background color picker" + /> + <Input + id="background-color-text" + value={backgroundColor} + onChange={(e) => setBackgroundColor(e.target.value)} + className="flex-1" + aria-label="Background color hex value" + /> + </div> + </div> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div> + <label htmlFor="corner-style" className="block text-sm font-medium text-gray-700 mb-2"> + {t.generator.corners} + </label> + <select + id="corner-style" + value={cornerStyle} + onChange={(e) => setCornerStyle(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + > + <option value="square">Square</option> + <option value="rounded">Rounded</option> + </select> + </div> + + <div> + <label htmlFor="qr-size" className="block text-sm font-medium text-gray-700 mb-2"> + {t.generator.size} + </label> + <input + id="qr-size" + type="range" + min="100" + max="400" + value={size} + onChange={(e) => setSize(Number(e.target.value))} + className="w-full accent-primary-600" + aria-label={`QR code size: ${size} pixels`} + /> + <div className="text-sm text-gray-500 text-center mt-1" aria-hidden="true">{size}px</div> + </div> + </div> + + <div className="flex items-center justify-between"> + <Badge variant={hasGoodContrast ? 'success' : 'warning'}> + {hasGoodContrast ? t.generator.contrast_good : 'Low contrast'} + </Badge> + <div className="text-sm text-gray-500"> + Contrast: {contrast.toFixed(1)}:1 + </div> + </div> + + <div className="flex space-x-3"> + <Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('svg')}> + {t.generator.download_svg} + </Button> + <Button variant="outline" className="flex-1 hover:bg-gray-50" onClick={() => downloadQR('png')}> + {t.generator.download_png} + </Button> + </div> + + <Button className="w-full text-lg py-6 shadow-lg shadow-primary-500/20 hover:shadow-primary-500/40 transition-all" onClick={() => window.location.href = '/login'}> + {t.generator.save_track} + </Button> + </Card> + </motion.div> + + {/* Right Preview */} + <motion.div + initial={{ opacity: 0, x: 20 }} + whileInView={{ opacity: 1, x: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: 0.4 }} + className="flex flex-col items-center justify-center p-8 bg-gradient-to-br from-blue-50/50 to-purple-50/50 rounded-2xl border border-blue-100/50 shadow-lg shadow-blue-500/5 relative overflow-hidden backdrop-blur-sm" + > + {/* Artistic Curved Lines Background */} + <div className="absolute inset-0 opacity-[0.4]"> + <svg className="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none"> + <defs> + <linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%"> + <stop offset="0%" stopColor="#60a5fa" stopOpacity="0.4" /> + <stop offset="100%" stopColor="#c084fc" stopOpacity="0.4" /> + </linearGradient> + <linearGradient id="gradient2" x1="100%" y1="0%" x2="0%" y2="100%"> + <stop offset="0%" stopColor="#818cf8" stopOpacity="0.4" /> + <stop offset="100%" stopColor="#38bdf8" stopOpacity="0.4" /> + </linearGradient> + </defs> + <path d="M0 100 Q 25 30 50 70 T 100 0" fill="none" stroke="url(#gradient1)" strokeWidth="0.8" className="opacity-60" /> + <path d="M0 50 Q 40 80 70 30 T 100 50" fill="none" stroke="url(#gradient2)" strokeWidth="0.8" className="opacity-60" /> + <path d="M0 0 Q 30 60 60 20 T 100 80" fill="none" stroke="url(#gradient1)" strokeWidth="0.6" className="opacity-40" /> + </svg> + <div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-15 brightness-100 contrast-150 mix-blend-overlay"></div> + </div> + + {/* Decorative Orbs */} + <div className="absolute -top-20 -right-20 w-64 h-64 bg-purple-200/30 rounded-full blur-3xl animate-blob"></div> + <div className="absolute -bottom-20 -left-20 w-64 h-64 bg-blue-200/30 rounded-full blur-3xl animate-blob animation-delay-2000"></div> + + <div className="text-center w-full relative z-10"> + <h3 className="text-xl font-bold mb-8 text-gray-800">{t.generator.live_preview}</h3> + <div id="instant-qr-preview" className="flex justify-center mb-8 transform hover:scale-105 transition-transform duration-300"> + {url ? ( + <div className={`${cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''} p-4 bg-white shadow-lg rounded-xl`}> + <QRCodeSVG + value={url} + size={size} + fgColor={foregroundColor} + bgColor={backgroundColor} + level="M" + /> + </div> + ) : ( + <div + className="bg-gray-100 rounded-xl flex items-center justify-center text-gray-500 animate-pulse" + style={{ width: 200, height: 200 }} + > + Enter URL + </div> + )} + </div> + <div className="text-sm font-medium text-gray-600 mb-2 bg-gray-50 py-2 px-4 rounded-full inline-block"> + {url || 'https://example.com'} + </div> + <div className="text-xs text-gray-400 mt-2">{t.generator.demo_note}</div> + </div> + </motion.div> + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/components/marketing/Pricing.tsx b/src/components/marketing/Pricing.tsx index 911b8fc..9f9671c 100644 --- a/src/components/marketing/Pricing.tsx +++ b/src/components/marketing/Pricing.tsx @@ -1,141 +1,141 @@ -'use client'; - -import React, { useState } from 'react'; -import { motion } from 'framer-motion'; -import Link from 'next/link'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; -import { Badge } from '@/components/ui/Badge'; -import { BillingToggle } from '@/components/ui/BillingToggle'; - -interface PricingProps { - t: any; // i18n translation function -} - -export const Pricing: React.FC<PricingProps> = ({ t }) => { - const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); - - const plans = [ - { - key: 'free', - popular: false, - }, - { - key: 'pro', - popular: true, - }, - { - key: 'business', - popular: false, - }, - ]; - - return ( - <section id="pricing" className="py-16"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <motion.div - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5 }} - className="text-center mb-12" - > - <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> - {t.pricing.title} - </h2> - <p className="text-xl text-gray-600"> - {t.pricing.subtitle} - </p> - </motion.div> - - <div className="flex justify-center mb-8"> - <BillingToggle value={billingPeriod} onChange={setBillingPeriod} /> - </div> - - <div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto"> - {plans.map((plan, index) => ( - <motion.div - key={plan.key} - initial={{ opacity: 0, y: 20 }} - whileInView={{ opacity: 1, y: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: index * 0.1 }} - className="h-full" - > - <Card - className={`h-full flex flex-col ${plan.popular - ? 'border-primary-500 shadow-xl relative scale-105 z-10' - : 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all' - }`} - > - {plan.popular && ( - <div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center"> - <Badge variant="info" className="px-4 py-1.5 shadow-sm"> - {t.pricing[plan.key].badge} - </Badge> - </div> - )} - - <CardHeader className="text-center pb-8"> - <CardTitle className="text-2xl mb-4"> - {t.pricing[plan.key].title} - </CardTitle> - <div className="flex flex-col items-center"> - <div className="flex items-baseline justify-center"> - <span className="text-4xl font-bold"> - {plan.key === 'free' - ? t.pricing[plan.key].price - : billingPeriod === 'month' - ? t.pricing[plan.key].price - : plan.key === 'pro' - ? '€90' - : '€290'} - </span> - <span className="text-gray-600 ml-2"> - {plan.key === 'free' - ? t.pricing[plan.key].period - : billingPeriod === 'month' - ? t.pricing[plan.key].period - : 'per year'} - </span> - </div> - {billingPeriod === 'year' && plan.key !== 'free' && ( - <Badge variant="success" className="mt-2"> - Save 16% - </Badge> - )} - </div> - </CardHeader> - - <CardContent className="space-y-8 flex-1 flex flex-col"> - <ul className="space-y-3 flex-1"> - {t.pricing[plan.key].features.map((feature: string, index: number) => ( - <li key={index} className="flex items-start space-x-3"> - <svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> - <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> - </svg> - <span className="text-gray-700">{feature}</span> - </li> - ))} - </ul> - - <div className="mt-8 pt-8 border-t border-gray-100"> - <Link href="/signup"> - <Button - variant={plan.popular ? 'primary' : 'outline'} - className="w-full" - size="lg" - > - Get Started - </Button> - </Link> - </div> - </CardContent> - </Card> - </motion.div> - ))} - </div> - </div> - </section> - ); +'use client'; + +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import Link from 'next/link'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; +import { BillingToggle } from '@/components/ui/BillingToggle'; + +interface PricingProps { + t: any; // i18n translation function +} + +export const Pricing: React.FC<PricingProps> = ({ t }) => { + const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); + + const plans = [ + { + key: 'free', + popular: false, + }, + { + key: 'pro', + popular: true, + }, + { + key: 'business', + popular: false, + }, + ]; + + return ( + <section id="pricing" className="py-16"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <motion.div + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5 }} + className="text-center mb-12" + > + <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> + {t.pricing.title} + </h2> + <p className="text-xl text-gray-600"> + {t.pricing.subtitle} + </p> + </motion.div> + + <div className="flex justify-center mb-8"> + <BillingToggle value={billingPeriod} onChange={setBillingPeriod} /> + </div> + + <div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto"> + {plans.map((plan, index) => ( + <motion.div + key={plan.key} + initial={{ opacity: 0, y: 20 }} + whileInView={{ opacity: 1, y: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: index * 0.1 }} + className="h-full" + > + <Card + className={`h-full flex flex-col ${plan.popular + ? 'border-primary-500 shadow-xl relative scale-105 z-10' + : 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all' + }`} + > + {plan.popular && ( + <div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center"> + <Badge variant="info" className="px-4 py-1.5 shadow-sm"> + {t.pricing[plan.key].badge} + </Badge> + </div> + )} + + <CardHeader className="text-center pb-8"> + <CardTitle className="text-2xl mb-4"> + {t.pricing[plan.key].title} + </CardTitle> + <div className="flex flex-col items-center"> + <div className="flex items-baseline justify-center"> + <span className="text-4xl font-bold"> + {plan.key === 'free' + ? t.pricing[plan.key].price + : billingPeriod === 'month' + ? t.pricing[plan.key].price + : plan.key === 'pro' + ? '€90' + : '€290'} + </span> + <span className="text-gray-600 ml-2"> + {plan.key === 'free' + ? t.pricing[plan.key].period + : billingPeriod === 'month' + ? t.pricing[plan.key].period + : 'per year'} + </span> + </div> + {billingPeriod === 'year' && plan.key !== 'free' && ( + <Badge variant="success" className="mt-2"> + Save 16% + </Badge> + )} + </div> + </CardHeader> + + <CardContent className="space-y-8 flex-1 flex flex-col"> + <ul className="space-y-3 flex-1"> + {t.pricing[plan.key].features.map((feature: string, index: number) => ( + <li key={index} className="flex items-start space-x-3"> + <svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> + <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> + </svg> + <span className="text-gray-700">{feature}</span> + </li> + ))} + </ul> + + <div className="mt-8 pt-8 border-t border-gray-100"> + <Link href="/signup"> + <Button + variant={plan.popular ? 'primary' : 'outline'} + className="w-full" + size="lg" + > + Get Started + </Button> + </Link> + </div> + </CardContent> + </Card> + </motion.div> + ))} + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/components/marketing/StaticVsDynamic.tsx b/src/components/marketing/StaticVsDynamic.tsx index 2579cf7..bd0329a 100644 --- a/src/components/marketing/StaticVsDynamic.tsx +++ b/src/components/marketing/StaticVsDynamic.tsx @@ -1,98 +1,98 @@ -'use client'; - -import React from 'react'; -import { motion } from 'framer-motion'; -import { CheckCircle2 } from 'lucide-react'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; -import { Badge } from '@/components/ui/Badge'; - -interface StaticVsDynamicProps { - t: any; // i18n translation function -} - -export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => { - return ( - <section className="py-16"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-12"> - <h2 className="text-3xl font-bold text-gray-900 sm:text-4xl mb-4"> - {t.static_vs_dynamic.title} - </h2> - <p className="text-lg text-gray-600 max-w-2xl mx-auto"> - {t.static_vs_dynamic.description} - </p> - </div> - - <div className="grid lg:grid-cols-2 gap-8 max-w-6xl mx-auto"> - {/* Static QR Codes */} - <motion.div - initial={{ opacity: 0, x: -20 }} - whileInView={{ opacity: 1, x: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: 0.2 }} - > - <Card className="relative h-full border-gray-200 shadow-sm hover:shadow-lg transition-all duration-300"> - <CardHeader> - <div className="flex items-center justify-between mb-2"> - <CardTitle className="text-2xl font-bold text-gray-700">{t.static_vs_dynamic.static.title}</CardTitle> - <Badge variant="success" className="bg-gray-100 text-gray-700 hover:bg-gray-200">{t.static_vs_dynamic.static.subtitle}</Badge> - </div> - <p className="text-gray-500">{t.static_vs_dynamic.static.description}</p> - </CardHeader> - <CardContent> - <ul className="space-y-4"> - {t.static_vs_dynamic.static.features.map((feature: string, index: number) => ( - <li key={index} className="flex items-start space-x-3"> - <div className="flex-shrink-0 w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center mt-0.5"> - <CheckCircle2 className="w-4 h-4 text-gray-500" /> - </div> - <span className="text-gray-600">{feature}</span> - </li> - ))} - </ul> - </CardContent> - </Card> - </motion.div> - - {/* Dynamic QR Codes */} - <motion.div - initial={{ opacity: 0, x: 20 }} - whileInView={{ opacity: 1, x: 0 }} - viewport={{ once: true }} - transition={{ duration: 0.5, delay: 0.2 }} - > - <Card className="relative h-full border-2 border-primary-500/20 bg-gradient-to-br from-white to-primary-50/50 shadow-xl shadow-primary-500/10 hover:shadow-2xl hover:shadow-primary-500/20 transition-all duration-300"> - <div className="absolute top-0 right-0 p-4"> - <div className="absolute -top-3 -right-3"> - <span className="relative flex h-4 w-4"> - <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary-400 opacity-75"></span> - <span className="relative inline-flex rounded-full h-4 w-4 bg-primary-500"></span> - </span> - </div> - </div> - <CardHeader> - <div className="flex items-center justify-between mb-2"> - <CardTitle className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary-600 to-purple-600">{t.static_vs_dynamic.dynamic.title}</CardTitle> - <Badge variant="info" className="bg-primary-100 text-primary-700">{t.static_vs_dynamic.dynamic.subtitle}</Badge> - </div> - <p className="text-gray-600 font-medium">{t.static_vs_dynamic.dynamic.description}</p> - </CardHeader> - <CardContent> - <ul className="space-y-4"> - {t.static_vs_dynamic.dynamic.features.map((feature: string, index: number) => ( - <li key={index} className="flex items-start space-x-3"> - <div className="flex-shrink-0 w-6 h-6 bg-primary-100 rounded-full flex items-center justify-center mt-0.5"> - <CheckCircle2 className="w-4 h-4 text-primary-600" /> - </div> - <span className="text-gray-900 font-medium">{feature}</span> - </li> - ))} - </ul> - </CardContent> - </Card> - </motion.div> - </div> - </div> - </section> - ); +'use client'; + +import React from 'react'; +import { motion } from 'framer-motion'; +import { CheckCircle2 } from 'lucide-react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; +import { Badge } from '@/components/ui/Badge'; + +interface StaticVsDynamicProps { + t: any; // i18n translation function +} + +export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => { + return ( + <section className="py-16"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-12"> + <h2 className="text-3xl font-bold text-gray-900 sm:text-4xl mb-4"> + {t.static_vs_dynamic.title} + </h2> + <p className="text-lg text-gray-600 max-w-2xl mx-auto"> + {t.static_vs_dynamic.description} + </p> + </div> + + <div className="grid lg:grid-cols-2 gap-8 max-w-6xl mx-auto"> + {/* Static QR Codes */} + <motion.div + initial={{ opacity: 0, x: -20 }} + whileInView={{ opacity: 1, x: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: 0.2 }} + > + <Card className="relative h-full border-gray-200 shadow-sm hover:shadow-lg transition-all duration-300"> + <CardHeader> + <div className="flex items-center justify-between mb-2"> + <CardTitle className="text-2xl font-bold text-gray-700">{t.static_vs_dynamic.static.title}</CardTitle> + <Badge variant="success" className="bg-gray-100 text-gray-700 hover:bg-gray-200">{t.static_vs_dynamic.static.subtitle}</Badge> + </div> + <p className="text-gray-500">{t.static_vs_dynamic.static.description}</p> + </CardHeader> + <CardContent> + <ul className="space-y-4"> + {t.static_vs_dynamic.static.features.map((feature: string, index: number) => ( + <li key={index} className="flex items-start space-x-3"> + <div className="flex-shrink-0 w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center mt-0.5"> + <CheckCircle2 className="w-4 h-4 text-gray-500" /> + </div> + <span className="text-gray-600">{feature}</span> + </li> + ))} + </ul> + </CardContent> + </Card> + </motion.div> + + {/* Dynamic QR Codes */} + <motion.div + initial={{ opacity: 0, x: 20 }} + whileInView={{ opacity: 1, x: 0 }} + viewport={{ once: true }} + transition={{ duration: 0.5, delay: 0.2 }} + > + <Card className="relative h-full border-2 border-primary-500/20 bg-gradient-to-br from-white to-primary-50/50 shadow-xl shadow-primary-500/10 hover:shadow-2xl hover:shadow-primary-500/20 transition-all duration-300"> + <div className="absolute top-0 right-0 p-4"> + <div className="absolute -top-3 -right-3"> + <span className="relative flex h-4 w-4"> + <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary-400 opacity-75"></span> + <span className="relative inline-flex rounded-full h-4 w-4 bg-primary-500"></span> + </span> + </div> + </div> + <CardHeader> + <div className="flex items-center justify-between mb-2"> + <CardTitle className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary-600 to-purple-600">{t.static_vs_dynamic.dynamic.title}</CardTitle> + <Badge variant="info" className="bg-primary-100 text-primary-700">{t.static_vs_dynamic.dynamic.subtitle}</Badge> + </div> + <p className="text-gray-600 font-medium">{t.static_vs_dynamic.dynamic.description}</p> + </CardHeader> + <CardContent> + <ul className="space-y-4"> + {t.static_vs_dynamic.dynamic.features.map((feature: string, index: number) => ( + <li key={index} className="flex items-start space-x-3"> + <div className="flex-shrink-0 w-6 h-6 bg-primary-100 rounded-full flex items-center justify-center mt-0.5"> + <CheckCircle2 className="w-4 h-4 text-primary-600" /> + </div> + <span className="text-gray-900 font-medium">{feature}</span> + </li> + ))} + </ul> + </CardContent> + </Card> + </motion.div> + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/components/marketing/StatsStrip.tsx b/src/components/marketing/StatsStrip.tsx index 82aa87f..e275589 100644 --- a/src/components/marketing/StatsStrip.tsx +++ b/src/components/marketing/StatsStrip.tsx @@ -1,35 +1,35 @@ -'use client'; - -import React from 'react'; - -interface StatsStripProps { - t: any; // i18n translation function -} - -export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => { - const stats = [ - { key: 'users', value: '1,240+', label: t.trust.users }, - { key: 'codes', value: '8,500+', label: t.trust.codes }, - { key: 'scans', value: '1.2M+', label: t.trust.scans }, - { key: 'countries', value: '120+', label: t.trust.countries }, - ]; - - return ( - <section className="py-20 bg-white border-y border-gray-100"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="grid grid-cols-2 lg:grid-cols-4 gap-8"> - {stats.map((stat, index) => ( - <div key={stat.key} className="text-center group hover:-translate-y-1 transition-transform duration-300"> - <div className="text-4xl lg:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-purple-600 mb-2"> - {stat.value} - </div> - <div className="text-gray-500 font-medium uppercase tracking-wider text-sm"> - {stat.label} - </div> - </div> - ))} - </div> - </div> - </section> - ); +'use client'; + +import React from 'react'; + +interface StatsStripProps { + t: any; // i18n translation function +} + +export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => { + const stats = [ + { key: 'users', value: '1,240+', label: t.trust.users }, + { key: 'codes', value: '8,500+', label: t.trust.codes }, + { key: 'scans', value: '1.2M+', label: t.trust.scans }, + { key: 'countries', value: '120+', label: t.trust.countries }, + ]; + + return ( + <section className="py-20 bg-white border-y border-gray-100"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="grid grid-cols-2 lg:grid-cols-4 gap-8"> + {stats.map((stat, index) => ( + <div key={stat.key} className="text-center group hover:-translate-y-1 transition-transform duration-300"> + <div className="text-4xl lg:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-purple-600 mb-2"> + {stat.value} + </div> + <div className="text-gray-500 font-medium uppercase tracking-wider text-sm"> + {stat.label} + </div> + </div> + ))} + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/components/marketing/TemplateCards.tsx b/src/components/marketing/TemplateCards.tsx index a8b97c4..bbb7fce 100644 --- a/src/components/marketing/TemplateCards.tsx +++ b/src/components/marketing/TemplateCards.tsx @@ -1,70 +1,70 @@ -'use client'; - -import React from 'react'; -import { Card } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; - -interface TemplateCardsProps { - t: any; // i18n translation function -} - -export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => { - const templates = [ - { - key: 'restaurant', - title: t.templates.restaurant, - icon: 'đŸœïž', - color: 'bg-red-50 border-red-200', - iconBg: 'bg-red-100', - }, - { - key: 'business', - title: t.templates.business, - icon: 'đŸ’Œ', - color: 'bg-blue-50 border-blue-200', - iconBg: 'bg-blue-100', - }, - { - key: 'vcard', - title: t.templates.vcard, - icon: 'đŸ‘€', - color: 'bg-purple-50 border-purple-200', - iconBg: 'bg-purple-100', - }, - { - key: 'event', - title: t.templates.event, - icon: 'đŸŽ«', - color: 'bg-green-50 border-green-200', - iconBg: 'bg-green-100', - }, - ]; - - return ( - <section className="py-16"> - <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> - <div className="text-center mb-12"> - <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> - {t.templates.title} - </h2> - </div> - - <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6"> - {templates.map((template) => ( - <Card key={template.key} className={`${template.color} text-center hover:scale-105 transition-transform cursor-pointer`}> - <div className={`w-16 h-16 ${template.iconBg} rounded-full flex items-center justify-center mx-auto mb-4`}> - <span className="text-2xl">{template.icon}</span> - </div> - <h3 className="text-lg font-semibold text-gray-900 mb-4"> - {template.title} - </h3> - <Button variant="outline" size="sm" className="w-full"> - {t.templates.use_template} - </Button> - </Card> - ))} - </div> - </div> - </section> - ); +'use client'; + +import React from 'react'; +import { Card } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; + +interface TemplateCardsProps { + t: any; // i18n translation function +} + +export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => { + const templates = [ + { + key: 'restaurant', + title: t.templates.restaurant, + icon: 'đŸœïž', + color: 'bg-red-50 border-red-200', + iconBg: 'bg-red-100', + }, + { + key: 'business', + title: t.templates.business, + icon: 'đŸ’Œ', + color: 'bg-blue-50 border-blue-200', + iconBg: 'bg-blue-100', + }, + { + key: 'vcard', + title: t.templates.vcard, + icon: 'đŸ‘€', + color: 'bg-purple-50 border-purple-200', + iconBg: 'bg-purple-100', + }, + { + key: 'event', + title: t.templates.event, + icon: 'đŸŽ«', + color: 'bg-green-50 border-green-200', + iconBg: 'bg-green-100', + }, + ]; + + return ( + <section className="py-16"> + <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> + <div className="text-center mb-12"> + <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> + {t.templates.title} + </h2> + </div> + + <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6"> + {templates.map((template) => ( + <Card key={template.key} className={`${template.color} text-center hover:scale-105 transition-transform cursor-pointer`}> + <div className={`w-16 h-16 ${template.iconBg} rounded-full flex items-center justify-center mx-auto mb-4`}> + <span className="text-2xl">{template.icon}</span> + </div> + <h3 className="text-lg font-semibold text-gray-900 mb-4"> + {template.title} + </h3> + <Button variant="outline" size="sm" className="w-full"> + {t.templates.use_template} + </Button> + </Card> + ))} + </div> + </div> + </section> + ); }; \ No newline at end of file diff --git a/src/i18n/de.json b/src/i18n/de.json index 2189fca..d74ebdd 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -1,374 +1,374 @@ -{ - "nav": { - "features": "Funktionen", - "pricing": "Preise", - "faq": "FAQ", - "blog": "Blog", - "login": "Anmelden", - "dashboard": "Dashboard", - "create_qr": "QR erstellen", - "bulk_creation": "Massen-Erstellung", - "analytics": "Analytik", - "settings": "Einstellungen" - }, - "hero": { - "badge": "Kostenloser QR-Code-Generator", - "title": "Erstellen Sie QR-Codes, die ĂŒberall funktionieren", - "subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos fĂŒr immer.", - "features": [ - "Keine Kreditkarte zum Starten erforderlich", - "QR-Codes fĂŒr immer kostenlos erstellen", - "Erweiterte Verfolgung und Analytik", - "Individuelle Farben und Stile" - ], - "cta_primary": "QR-Code kostenlos erstellen", - "cta_secondary": "Preise ansehen", - "engagement_badge": "Kostenlos fĂŒr immer", - "get_started": "Loslegen", - "view_full_pricing": "Alle Preisdetails ansehen →" - }, - "trust": { - "users": "10.000+ Aktive Nutzer", - "codes": "500.000+ QR-Codes erstellt", - "scans": "50M+ Scans verfolgt", - "countries": "120+ LĂ€nder" - }, - "industries": { - "restaurant": "Restaurant-Kette", - "tech": "Tech-Startup", - "realestate": "Immobilien", - "events": "Event-Agentur", - "retail": "Einzelhandel", - "healthcare": "Gesundheitswesen" - }, - "templates": { - "title": "Mit einer Vorlage beginnen", - "restaurant": "Restaurant-MenĂŒ", - "business": "Visitenkarte", - "vcard": "Kontaktkarte", - "event": "Event-Ticket", - "use_template": "Vorlage verwenden →" - }, - "generator": { - "title": "Sofortiger QR-Code-Generator", - "url_placeholder": "Geben Sie hier Ihre URL ein...", - "foreground": "Vordergrund", - "background": "Hintergrund", - "corners": "Ecken", - "size": "GrĂ¶ĂŸe", - "contrast_good": "Guter Kontrast", - "download_svg": "SVG herunterladen", - "download_png": "PNG herunterladen", - "save_track": "Speichern & Verfolgen", - "live_preview": "Live-Vorschau", - "demo_note": "Dies ist ein Demo-QR-Code" - }, - "static_vs_dynamic": { - "static": { - "title": "Statische QR-Codes", - "subtitle": "Immer kostenlos", - "description": "Perfekt fĂŒr permanente Inhalte, die sich nie Ă€ndern", - "features": [ - "Inhalt kann nicht bearbeitet werden", - "Keine Scan-Verfolgung", - "Funktioniert fĂŒr immer", - "Kein Konto erforderlich" - ] - }, - "dynamic": { - "title": "Dynamische QR-Codes", - "subtitle": "Empfohlen", - "description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen", - "features": [ - "Inhalt jederzeit bearbeiten", - "Erweiterte Analytik", - "Individuelles Branding", - "Bulk-Operationen" - ] - } - }, - "features": { - "title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen", - "analytics": { - "title": "Erweiterte Analytik", - "description": "Verfolgen Sie Scans, Standorte, GerĂ€te und Nutzerverhalten mit detaillierten Einblicken." - }, - "customization": { - "title": "VollstĂ€ndige Anpassung", - "description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen." - }, - "bulk": { - "title": "Bulk-Operationen", - "description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung." - }, - "integrations": { - "title": "Integrationen", - "description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools." - }, - "api": { - "title": "Entwickler-API", - "description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API." - }, - "support": { - "title": "24/7 Support", - "description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team." - } - }, - "pricing": { - "title": "WĂ€hlen Sie Ihren Plan", - "subtitle": "WĂ€hlen Sie den perfekten Plan fĂŒr Ihre QR-Code-BedĂŒrfnisse", - "choose_plan": "WĂ€hlen Sie Ihren Plan", - "select_plan": "WĂ€hlen Sie den perfekten Plan fĂŒr Ihre QR-Code-BedĂŒrfnisse", - "current_plan": "Aktueller Plan", - "upgrade_to": "Upgrade auf", - "downgrade_to_free": "Zu Kostenlos zurĂŒckstufen", - "most_popular": "Beliebteste", - "all_plans_note": "Alle PlĂ€ne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.", - "free": { - "title": "Kostenlos", - "name": "Free", - "price": "€0", - "period": "fĂŒr immer", - "features": [ - "3 dynamische QR-Codes", - "Unbegrenzte statische QR-Codes", - "Basis-Scan-Tracking", - "Standard QR-Design-Vorlagen" - ] - }, - "pro": { - "title": "Pro", - "name": "Pro", - "price": "€9", - "period": "pro Monat", - "badge": "Beliebteste", - "features": [ - "50 dynamische QR-Codes", - "Unbegrenzte statische QR-Codes", - "Erweiterte Analytik (Scans, GerĂ€te, Standorte)", - "Individuelles Branding (Farben)", - "Download als SVG/PNG" - ] - }, - "business": { - "title": "Business", - "name": "Business", - "price": "€29", - "period": "pro Monat", - "features": [ - "500 dynamische QR-Codes", - "Unbegrenzte statische QR-Codes", - "Alles aus Pro", - "Massen-QR-Erstellung (bis zu 1.000)", - "PrioritĂ€ts-E-Mail-Support", - "Erweiterte Tracking & Insights" - ] - } - }, - "faq": { - "title": "HĂ€ufig gestellte Fragen", - "questions": { - "account": { - "question": "Benötige ich ein Konto, um QR-Codes zu erstellen?", - "answer": "FĂŒr statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto." - }, - "static_vs_dynamic": { - "question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?", - "answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geĂ€ndert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik." - }, - "forever": { - "question": "Funktionieren meine QR-Codes fĂŒr immer?", - "answer": "Statische QR-Codes funktionieren fĂŒr immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist." - }, - "file_type": { - "question": "Welchen Dateityp sollte ich zum Drucken verwenden?", - "answer": "FĂŒr Druckmaterialien empfehlen wir das SVG-Format fĂŒr Skalierbarkeit oder hochauflösendes PNG (300+ DPI) fĂŒr beste QualitĂ€t." - }, - "password": { - "question": "Kann ich einen QR-Code mit einem Passwort schĂŒtzen?", - "answer": "Ja, Pro- und Business-PlĂ€ne beinhalten Passwortschutz und Zugriffskontrollfunktionen fĂŒr Ihre QR-Codes." - }, - "analytics": { - "question": "Wie funktioniert die Analytik?", - "answer": "Wir verfolgen Scans, Standorte, GerĂ€te und Referrer unter Beachtung der PrivatsphĂ€re der Nutzer. Keine persönlichen Daten werden gespeichert." - }, - "privacy": { - "question": "Verfolgen Sie persönliche Daten?", - "answer": "Wir respektieren die PrivatsphĂ€re und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header." - }, - "bulk": { - "question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?", - "answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen." - } - } - }, - "dashboard": { - "title": "Dashboard", - "subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance", - "stats": { - "total_scans": "Gesamte Scans", - "active_codes": "Aktive QR-Codes", - "conversion_rate": "Konversionsrate" - }, - "recent_codes": "Aktuelle QR-Codes", - "blog_resources": "Blog & Ressourcen", - "menu": { - "edit": "Bearbeiten", - "duplicate": "Duplizieren", - "pause": "Pausieren", - "delete": "Löschen" - } - }, - "create": { - "title": "QR-Code erstellen", - "subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding", - "content": "Inhalt", - "type": "QR-Code-Typ", - "style": "Stil & Branding", - "preview": "Live-Vorschau", - "title_label": "Titel", - "title_placeholder": "Mein QR-Code", - "content_type": "Inhaltstyp", - "url_label": "URL", - "url_placeholder": "https://beispiel.de", - "tags_label": "Tags (durch Komma getrennt)", - "tags_placeholder": "marketing, kampagne, 2025", - "qr_code_type": "QR-Code-Typ", - "dynamic": "Dynamisch", - "static": "Statisch", - "recommended": "Empfohlen", - "dynamic_description": "Dynamisch: Scans verfolgen, URL spĂ€ter bearbeiten, Analytik ansehen. QR enthĂ€lt Tracking-Link.", - "static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthĂ€lt tatsĂ€chlichen Inhalt.", - "foreground_color": "Vordergrundfarbe", - "background_color": "Hintergrundfarbe", - "corner_style": "Eckenstil", - "size": "GrĂ¶ĂŸe", - "good_contrast": "Guter Kontrast", - "contrast_ratio": "KontrastverhĂ€ltnis", - "download_svg": "SVG herunterladen", - "download_png": "PNG herunterladen", - "save_qr_code": "QR-Code speichern" - }, - "analytics": { - "title": "Analytik", - "subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes", - "export_report": "Bericht exportieren", - "from_last_period": "vom letzten Zeitraum", - "no_mobile_scans": "Keine mobilen Scans", - "of_total": "der Gesamtmenge", - "ranges": { - "7d": "7 Tage", - "30d": "30 Tage", - "90d": "90 Tage" - }, - "kpis": { - "total_scans": "Gesamte Scans", - "avg_scans": "Ø Scans/QR", - "mobile_usage": "Mobile Nutzung", - "top_country": "Top Land" - }, - "charts": { - "scans_over_time": "Scans ĂŒber Zeit", - "device_types": "GerĂ€tetypen", - "top_countries": "Top LĂ€nder" - }, - "table": { - "qr_code": "QR-Code", - "type": "Typ", - "total_scans": "Gesamte Scans", - "unique_scans": "Einzigartige Scans", - "conversion": "Konversion", - "trend": "Trend", - "scans": "Scans", - "percentage": "Prozent", - "country": "Land", - "performance": "Performance", - "created": "Erstellt", - "status": "Status" - }, - "performance_title": "QR-Code-Performance" - }, - "bulk": { - "title": "Massen-Erstellung", - "subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien", - "template_warning_title": "Bitte folgen Sie dem Vorlagenformat", - "template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten fĂŒr Titel und Inhalt (URL) enthalten.", - "static_only_title": "Nur statische QR-Codes", - "static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt fĂŒr Druckmaterialien und Offline-Nutzung.", - "download_template": "Vorlage herunterladen", - "no_file_selected": "Keine ausgewĂ€hlt", - "simple_format": "Einfaches Format", - "just_title_url": "Nur Titel & URL", - "static_qr_codes": "Statische QR-Codes", - "no_tracking": "Kein Tracking enthalten", - "instant_download": "Sofortiger Download", - "get_zip": "ZIP mit allen SVGs erhalten", - "max_rows": "max 1.000 Zeilen", - "steps": { - "upload": "Datei hochladen", - "preview": "Vorschau & Zuordnung", - "download": "Herunterladen" - }, - "drag_drop": "Datei hier hinziehen", - "or_click": "oder klicken zum Durchsuchen", - "supported_formats": "UnterstĂŒtzt CSV, XLS, XLSX (max 1.000 Zeilen)" - }, - "integrations": { - "title": "Integrationen", - "metrics": { - "total_codes": "QR-Codes Gesamt", - "active_integrations": "Aktive Integrationen", - "sync_status": "Sync-Status", - "available_services": "VerfĂŒgbare Services" - }, - "zapier": { - "title": "Zapier", - "description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps", - "features": [ - "Trigger bei neuen QR-Codes", - "Codes aus anderen Apps erstellen", - "Scan-Daten synchronisieren" - ] - }, - "airtable": { - "title": "Airtable", - "description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen", - "features": [ - "Bidirektionale Synchronisation", - "Individuelle Feldzuordnung", - "Echtzeit-Updates" - ] - }, - "sheets": { - "title": "Google Sheets", - "description": "Exportieren Sie Daten automatisch zu Google Sheets", - "features": [ - "Automatisierte Exporte", - "Individuelle Vorlagen", - "Geplante Updates" - ] - }, - "activate": "Aktivieren & Konfigurieren" - }, - "settings": { - "title": "Einstellungen", - "subtitle": "Verwalten Sie Ihre Kontoeinstellungen und PrĂ€ferenzen", - "tabs": { - "profile": "Profil", - "billing": "Abrechnung", - "team": "Team & Rollen", - "api": "API-SchlĂŒssel", - "workspace": "Arbeitsbereich" - } - }, - "common": { - "save": "Speichern", - "cancel": "Abbrechen", - "delete": "Löschen", - "edit": "Bearbeiten", - "create": "Erstellen", - "loading": "LĂ€dt...", - "error": "Ein Fehler ist aufgetreten", - "success": "Erfolgreich!" - } +{ + "nav": { + "features": "Funktionen", + "pricing": "Preise", + "faq": "FAQ", + "blog": "Blog", + "login": "Anmelden", + "dashboard": "Dashboard", + "create_qr": "QR erstellen", + "bulk_creation": "Massen-Erstellung", + "analytics": "Analytik", + "settings": "Einstellungen" + }, + "hero": { + "badge": "Kostenloser QR-Code-Generator", + "title": "Erstellen Sie QR-Codes, die ĂŒberall funktionieren", + "subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos fĂŒr immer.", + "features": [ + "Keine Kreditkarte zum Starten erforderlich", + "QR-Codes fĂŒr immer kostenlos erstellen", + "Erweiterte Verfolgung und Analytik", + "Individuelle Farben und Stile" + ], + "cta_primary": "QR-Code kostenlos erstellen", + "cta_secondary": "Preise ansehen", + "engagement_badge": "Kostenlos fĂŒr immer", + "get_started": "Loslegen", + "view_full_pricing": "Alle Preisdetails ansehen →" + }, + "trust": { + "users": "10.000+ Aktive Nutzer", + "codes": "500.000+ QR-Codes erstellt", + "scans": "50M+ Scans verfolgt", + "countries": "120+ LĂ€nder" + }, + "industries": { + "restaurant": "Restaurant-Kette", + "tech": "Tech-Startup", + "realestate": "Immobilien", + "events": "Event-Agentur", + "retail": "Einzelhandel", + "healthcare": "Gesundheitswesen" + }, + "templates": { + "title": "Mit einer Vorlage beginnen", + "restaurant": "Restaurant-MenĂŒ", + "business": "Visitenkarte", + "vcard": "Kontaktkarte", + "event": "Event-Ticket", + "use_template": "Vorlage verwenden →" + }, + "generator": { + "title": "Sofortiger QR-Code-Generator", + "url_placeholder": "Geben Sie hier Ihre URL ein...", + "foreground": "Vordergrund", + "background": "Hintergrund", + "corners": "Ecken", + "size": "GrĂ¶ĂŸe", + "contrast_good": "Guter Kontrast", + "download_svg": "SVG herunterladen", + "download_png": "PNG herunterladen", + "save_track": "Speichern & Verfolgen", + "live_preview": "Live-Vorschau", + "demo_note": "Dies ist ein Demo-QR-Code" + }, + "static_vs_dynamic": { + "static": { + "title": "Statische QR-Codes", + "subtitle": "Immer kostenlos", + "description": "Perfekt fĂŒr permanente Inhalte, die sich nie Ă€ndern", + "features": [ + "Inhalt kann nicht bearbeitet werden", + "Keine Scan-Verfolgung", + "Funktioniert fĂŒr immer", + "Kein Konto erforderlich" + ] + }, + "dynamic": { + "title": "Dynamische QR-Codes", + "subtitle": "Empfohlen", + "description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen", + "features": [ + "Inhalt jederzeit bearbeiten", + "Erweiterte Analytik", + "Individuelles Branding", + "Bulk-Operationen" + ] + } + }, + "features": { + "title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen", + "analytics": { + "title": "Erweiterte Analytik", + "description": "Verfolgen Sie Scans, Standorte, GerĂ€te und Nutzerverhalten mit detaillierten Einblicken." + }, + "customization": { + "title": "VollstĂ€ndige Anpassung", + "description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen." + }, + "bulk": { + "title": "Bulk-Operationen", + "description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung." + }, + "integrations": { + "title": "Integrationen", + "description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools." + }, + "api": { + "title": "Entwickler-API", + "description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API." + }, + "support": { + "title": "24/7 Support", + "description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team." + } + }, + "pricing": { + "title": "WĂ€hlen Sie Ihren Plan", + "subtitle": "WĂ€hlen Sie den perfekten Plan fĂŒr Ihre QR-Code-BedĂŒrfnisse", + "choose_plan": "WĂ€hlen Sie Ihren Plan", + "select_plan": "WĂ€hlen Sie den perfekten Plan fĂŒr Ihre QR-Code-BedĂŒrfnisse", + "current_plan": "Aktueller Plan", + "upgrade_to": "Upgrade auf", + "downgrade_to_free": "Zu Kostenlos zurĂŒckstufen", + "most_popular": "Beliebteste", + "all_plans_note": "Alle PlĂ€ne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.", + "free": { + "title": "Kostenlos", + "name": "Free", + "price": "€0", + "period": "fĂŒr immer", + "features": [ + "3 dynamische QR-Codes", + "Unbegrenzte statische QR-Codes", + "Basis-Scan-Tracking", + "Standard QR-Design-Vorlagen" + ] + }, + "pro": { + "title": "Pro", + "name": "Pro", + "price": "€9", + "period": "pro Monat", + "badge": "Beliebteste", + "features": [ + "50 dynamische QR-Codes", + "Unbegrenzte statische QR-Codes", + "Erweiterte Analytik (Scans, GerĂ€te, Standorte)", + "Individuelles Branding (Farben)", + "Download als SVG/PNG" + ] + }, + "business": { + "title": "Business", + "name": "Business", + "price": "€29", + "period": "pro Monat", + "features": [ + "500 dynamische QR-Codes", + "Unbegrenzte statische QR-Codes", + "Alles aus Pro", + "Massen-QR-Erstellung (bis zu 1.000)", + "PrioritĂ€ts-E-Mail-Support", + "Erweiterte Tracking & Insights" + ] + } + }, + "faq": { + "title": "HĂ€ufig gestellte Fragen", + "questions": { + "account": { + "question": "Benötige ich ein Konto, um QR-Codes zu erstellen?", + "answer": "FĂŒr statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto." + }, + "static_vs_dynamic": { + "question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?", + "answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geĂ€ndert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik." + }, + "forever": { + "question": "Funktionieren meine QR-Codes fĂŒr immer?", + "answer": "Statische QR-Codes funktionieren fĂŒr immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist." + }, + "file_type": { + "question": "Welchen Dateityp sollte ich zum Drucken verwenden?", + "answer": "FĂŒr Druckmaterialien empfehlen wir das SVG-Format fĂŒr Skalierbarkeit oder hochauflösendes PNG (300+ DPI) fĂŒr beste QualitĂ€t." + }, + "password": { + "question": "Kann ich einen QR-Code mit einem Passwort schĂŒtzen?", + "answer": "Ja, Pro- und Business-PlĂ€ne beinhalten Passwortschutz und Zugriffskontrollfunktionen fĂŒr Ihre QR-Codes." + }, + "analytics": { + "question": "Wie funktioniert die Analytik?", + "answer": "Wir verfolgen Scans, Standorte, GerĂ€te und Referrer unter Beachtung der PrivatsphĂ€re der Nutzer. Keine persönlichen Daten werden gespeichert." + }, + "privacy": { + "question": "Verfolgen Sie persönliche Daten?", + "answer": "Wir respektieren die PrivatsphĂ€re und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header." + }, + "bulk": { + "question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?", + "answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen." + } + } + }, + "dashboard": { + "title": "Dashboard", + "subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance", + "stats": { + "total_scans": "Gesamte Scans", + "active_codes": "Aktive QR-Codes", + "conversion_rate": "Konversionsrate" + }, + "recent_codes": "Aktuelle QR-Codes", + "blog_resources": "Blog & Ressourcen", + "menu": { + "edit": "Bearbeiten", + "duplicate": "Duplizieren", + "pause": "Pausieren", + "delete": "Löschen" + } + }, + "create": { + "title": "QR-Code erstellen", + "subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding", + "content": "Inhalt", + "type": "QR-Code-Typ", + "style": "Stil & Branding", + "preview": "Live-Vorschau", + "title_label": "Titel", + "title_placeholder": "Mein QR-Code", + "content_type": "Inhaltstyp", + "url_label": "URL", + "url_placeholder": "https://beispiel.de", + "tags_label": "Tags (durch Komma getrennt)", + "tags_placeholder": "marketing, kampagne, 2025", + "qr_code_type": "QR-Code-Typ", + "dynamic": "Dynamisch", + "static": "Statisch", + "recommended": "Empfohlen", + "dynamic_description": "Dynamisch: Scans verfolgen, URL spĂ€ter bearbeiten, Analytik ansehen. QR enthĂ€lt Tracking-Link.", + "static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthĂ€lt tatsĂ€chlichen Inhalt.", + "foreground_color": "Vordergrundfarbe", + "background_color": "Hintergrundfarbe", + "corner_style": "Eckenstil", + "size": "GrĂ¶ĂŸe", + "good_contrast": "Guter Kontrast", + "contrast_ratio": "KontrastverhĂ€ltnis", + "download_svg": "SVG herunterladen", + "download_png": "PNG herunterladen", + "save_qr_code": "QR-Code speichern" + }, + "analytics": { + "title": "Analytik", + "subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes", + "export_report": "Bericht exportieren", + "from_last_period": "vom letzten Zeitraum", + "no_mobile_scans": "Keine mobilen Scans", + "of_total": "der Gesamtmenge", + "ranges": { + "7d": "7 Tage", + "30d": "30 Tage", + "90d": "90 Tage" + }, + "kpis": { + "total_scans": "Gesamte Scans", + "avg_scans": "Ø Scans/QR", + "mobile_usage": "Mobile Nutzung", + "top_country": "Top Land" + }, + "charts": { + "scans_over_time": "Scans ĂŒber Zeit", + "device_types": "GerĂ€tetypen", + "top_countries": "Top LĂ€nder" + }, + "table": { + "qr_code": "QR-Code", + "type": "Typ", + "total_scans": "Gesamte Scans", + "unique_scans": "Einzigartige Scans", + "conversion": "Konversion", + "trend": "Trend", + "scans": "Scans", + "percentage": "Prozent", + "country": "Land", + "performance": "Performance", + "created": "Erstellt", + "status": "Status" + }, + "performance_title": "QR-Code-Performance" + }, + "bulk": { + "title": "Massen-Erstellung", + "subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien", + "template_warning_title": "Bitte folgen Sie dem Vorlagenformat", + "template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten fĂŒr Titel und Inhalt (URL) enthalten.", + "static_only_title": "Nur statische QR-Codes", + "static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt fĂŒr Druckmaterialien und Offline-Nutzung.", + "download_template": "Vorlage herunterladen", + "no_file_selected": "Keine ausgewĂ€hlt", + "simple_format": "Einfaches Format", + "just_title_url": "Nur Titel & URL", + "static_qr_codes": "Statische QR-Codes", + "no_tracking": "Kein Tracking enthalten", + "instant_download": "Sofortiger Download", + "get_zip": "ZIP mit allen SVGs erhalten", + "max_rows": "max 1.000 Zeilen", + "steps": { + "upload": "Datei hochladen", + "preview": "Vorschau & Zuordnung", + "download": "Herunterladen" + }, + "drag_drop": "Datei hier hinziehen", + "or_click": "oder klicken zum Durchsuchen", + "supported_formats": "UnterstĂŒtzt CSV, XLS, XLSX (max 1.000 Zeilen)" + }, + "integrations": { + "title": "Integrationen", + "metrics": { + "total_codes": "QR-Codes Gesamt", + "active_integrations": "Aktive Integrationen", + "sync_status": "Sync-Status", + "available_services": "VerfĂŒgbare Services" + }, + "zapier": { + "title": "Zapier", + "description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps", + "features": [ + "Trigger bei neuen QR-Codes", + "Codes aus anderen Apps erstellen", + "Scan-Daten synchronisieren" + ] + }, + "airtable": { + "title": "Airtable", + "description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen", + "features": [ + "Bidirektionale Synchronisation", + "Individuelle Feldzuordnung", + "Echtzeit-Updates" + ] + }, + "sheets": { + "title": "Google Sheets", + "description": "Exportieren Sie Daten automatisch zu Google Sheets", + "features": [ + "Automatisierte Exporte", + "Individuelle Vorlagen", + "Geplante Updates" + ] + }, + "activate": "Aktivieren & Konfigurieren" + }, + "settings": { + "title": "Einstellungen", + "subtitle": "Verwalten Sie Ihre Kontoeinstellungen und PrĂ€ferenzen", + "tabs": { + "profile": "Profil", + "billing": "Abrechnung", + "team": "Team & Rollen", + "api": "API-SchlĂŒssel", + "workspace": "Arbeitsbereich" + } + }, + "common": { + "save": "Speichern", + "cancel": "Abbrechen", + "delete": "Löschen", + "edit": "Bearbeiten", + "create": "Erstellen", + "loading": "LĂ€dt...", + "error": "Ein Fehler ist aufgetreten", + "success": "Erfolgreich!" + } } \ No newline at end of file diff --git a/src/lib/env.ts b/src/lib/env.ts index 2852df6..b09bfc8 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -11,6 +11,14 @@ const envSchema = z.object({ REDIS_URL: z.string().optional(), IP_SALT: z.string().default('development-salt-change-in-production'), ENABLE_DEMO: z.string().default('false'), + + // Cloudflare R2 (S3 Compatible) + R2_ACCOUNT_ID: z.string().optional(), + R2_ACCESS_KEY_ID: z.string().optional(), + R2_SECRET_ACCESS_KEY: z.string().optional(), + R2_BUCKET_NAME: z.string().default('qrmaster-menus'), + R2_PUBLIC_URL: z.string().optional(), + MAX_UPLOAD_SIZE: z.string().default('10485760'), // 10MB default }); // During build, we might not have all env vars, so we'll use defaults diff --git a/src/lib/r2.ts b/src/lib/r2.ts new file mode 100644 index 0000000..21e27f2 --- /dev/null +++ b/src/lib/r2.ts @@ -0,0 +1,65 @@ +import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; +import { env } from './env'; +import crypto from 'crypto'; + +// Initialize S3 client for Cloudflare R2 +const r2Client = new S3Client({ + region: 'auto', + endpoint: `https://${env.R2_ACCOUNT_ID || 'placeholder'}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: env.R2_ACCESS_KEY_ID || '', + secretAccessKey: env.R2_SECRET_ACCESS_KEY || '', + }, +}); + +export async function uploadFileToR2( + file: Buffer, + filename: string, + contentType: string = 'application/pdf' +): Promise<string> { + // Generate a unique key for the file + const ext = filename.split('.').pop() || 'pdf'; + const randomId = crypto.randomBytes(8).toString('hex'); + const timestamp = Date.now(); + const key = `uploads/${timestamp}_${randomId}.${ext}`; + + await r2Client.send( + new PutObjectCommand({ + Bucket: env.R2_BUCKET_NAME, + Key: key, + Body: file, + ContentType: contentType, + ContentDisposition: `inline; filename="${filename}"`, + // Cache for 1 year, as these are static files + CacheControl: 'public, max-age=31536000', + }) + ); + + // Return the public URL + // If R2_PUBLIC_URL is set, use it (custom domain or r2.dev subdomain) + // Otherwise, construct a fallback (which might not work without public access enabled on bucket) + const publicUrl = env.R2_PUBLIC_URL + ? `${env.R2_PUBLIC_URL}/${key}` + : `https://${env.R2_BUCKET_NAME}.r2.dev/${key}`; + + return publicUrl; +} + +export async function deleteFileFromR2(fileUrl: string): Promise<void> { + try { + // Extract key from URL + // URL format: https://domain.com/uploads/filename.pdf + const url = new URL(fileUrl); + const key = url.pathname.substring(1); // Remove leading slash + + await r2Client.send( + new DeleteObjectCommand({ + Bucket: env.R2_BUCKET_NAME, + Key: key, + }) + ); + } catch (error) { + console.error('Error deleting file from R2:', error); + // Suppress error, as deletion failure shouldn't block main flow + } +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index f41f47d..5d81360 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -1,245 +1,245 @@ -export interface BreadcrumbItem { - name: string; - url: string; -} - -export interface BlogPost { - title: string; - description: string; - slug: string; - author: string; - authorUrl: string; - datePublished: string; - dateModified: string; - image: string; -} - -export interface FAQItem { - question: string; - answer: string; -} - -export interface ProductOffer { - name: string; - price: string; - priceCurrency: string; - availability: string; - url: string; -} - -export interface HowToStep { - name: string; - text: string; - url?: string; -} - -export interface HowToTask { - name: string; - description: string; - steps: HowToStep[]; - totalTime?: string; -} - -export function organizationSchema() { - return { - '@context': 'https://schema.org', - '@type': 'Organization', - '@id': 'https://www.qrmaster.net/#organization', - name: 'QR Master', - alternateName: 'QRMaster', - url: 'https://www.qrmaster.net', - logo: { - '@type': 'ImageObject', - url: 'https://www.qrmaster.net/static/og-image.png', - width: 1200, - height: 630, - }, - image: 'https://www.qrmaster.net/static/og-image.png', - sameAs: [ - 'https://twitter.com/qrmaster', - ], - contactPoint: { - '@type': 'ContactPoint', - contactType: 'Customer Support', - email: 'support@qrmaster.net', - availableLanguage: ['English', 'German'], - }, - description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.', - slogan: 'Dynamic QR codes that work smarter', - foundingDate: '2025', - areaServed: 'Worldwide', - serviceType: 'Software as a Service', - priceRange: '$0 - $29', - knowsAbout: [ - 'QR Code Generation', - 'Marketing Analytics', - 'Campaign Tracking', - 'Dynamic QR Codes', - 'Bulk QR Generation', - ], - hasOfferCatalog: { - '@type': 'OfferCatalog', - name: 'QR Master Plans', - itemListElement: [ - { - '@type': 'Offer', - itemOffered: { - '@type': 'SoftwareApplication', - name: 'QR Master Free', - applicationCategory: 'BusinessApplication', - operatingSystem: 'Web Browser', - }, - }, - { - '@type': 'Offer', - itemOffered: { - '@type': 'SoftwareApplication', - name: 'QR Master Pro', - applicationCategory: 'BusinessApplication', - operatingSystem: 'Web Browser', - }, - }, - ], - }, - inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.net', - }; -} - -export function websiteSchema() { - return { - '@context': 'https://schema.org', - '@type': 'WebSite', - '@id': 'https://www.qrmaster.net/#website', - name: 'QR Master', - url: 'https://www.qrmaster.net', - inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.net', - publisher: { - '@id': 'https://www.qrmaster.net/#organization', - }, - potentialAction: { - '@type': 'SearchAction', - target: { - '@type': 'EntryPoint', - urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}', - }, - 'query-input': 'required name=search_term_string', - }, - }; -} - -export function breadcrumbSchema(items: BreadcrumbItem[]) { - return { - '@context': 'https://schema.org', - '@type': 'BreadcrumbList', - '@id': `https://www.qrmaster.net${items[items.length - 1]?.url}#breadcrumb`, - inLanguage: 'en', - mainEntityOfPage: `https://www.qrmaster.net${items[items.length - 1]?.url}`, - itemListElement: items.map((item, index) => ({ - '@type': 'ListItem', - position: index + 1, - name: item.name, - item: `https://www.qrmaster.net${item.url}`, - })), - }; -} - -export function blogPostingSchema(post: BlogPost) { - return { - '@context': 'https://schema.org', - '@type': 'BlogPosting', - '@id': `https://www.qrmaster.net/blog/${post.slug}#article`, - headline: post.title, - description: post.description, - image: post.image, - datePublished: post.datePublished, - dateModified: post.dateModified, - inLanguage: 'en', - mainEntityOfPage: `https://www.qrmaster.net/blog/${post.slug}`, - author: { - '@type': 'Person', - name: post.author, - url: post.authorUrl, - }, - publisher: { - '@type': 'Organization', - name: 'QR Master', - url: 'https://www.qrmaster.net', - logo: { - '@type': 'ImageObject', - url: 'https://www.qrmaster.net/static/og-image.png', - width: 1200, - height: 630, - }, - }, - isPartOf: { - '@type': 'Blog', - '@id': 'https://www.qrmaster.net/blog#blog', - name: 'QR Master Blog', - url: 'https://www.qrmaster.net/blog', - }, - }; -} - -export function faqPageSchema(faqs: FAQItem[]) { - return { - '@context': 'https://schema.org', - '@type': 'FAQPage', - '@id': 'https://www.qrmaster.net/faq#faqpage', - inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.net/faq', - mainEntity: faqs.map((faq) => ({ - '@type': 'Question', - name: faq.question, - acceptedAnswer: { - '@type': 'Answer', - text: faq.answer, - }, - })), - }; -} - -export function productSchema(product: { name: string; description: string; offers: ProductOffer[] }) { - return { - '@context': 'https://schema.org', - '@type': 'Product', - '@id': 'https://www.qrmaster.net/pricing#product', - name: product.name, - description: product.description, - inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.net/pricing', - brand: { - '@type': 'Organization', - name: 'QR Master', - }, - offers: product.offers.map((offer) => ({ - '@type': 'Offer', - name: offer.name, - price: offer.price, - priceCurrency: offer.priceCurrency, - availability: offer.availability, - url: offer.url, - })), - }; -} - -export function howToSchema(task: HowToTask) { - return { - '@context': 'https://schema.org', - '@type': 'HowTo', - '@id': `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`, - name: task.name, - description: task.description, - inLanguage: 'en', - mainEntityOfPage: `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`, - totalTime: task.totalTime || 'PT5M', - step: task.steps.map((step, index) => ({ - '@type': 'HowToStep', - position: index + 1, - name: step.name, - text: step.text, - url: step.url, - })), - }; -} +export interface BreadcrumbItem { + name: string; + url: string; +} + +export interface BlogPost { + title: string; + description: string; + slug: string; + author: string; + authorUrl: string; + datePublished: string; + dateModified: string; + image: string; +} + +export interface FAQItem { + question: string; + answer: string; +} + +export interface ProductOffer { + name: string; + price: string; + priceCurrency: string; + availability: string; + url: string; +} + +export interface HowToStep { + name: string; + text: string; + url?: string; +} + +export interface HowToTask { + name: string; + description: string; + steps: HowToStep[]; + totalTime?: string; +} + +export function organizationSchema() { + return { + '@context': 'https://schema.org', + '@type': 'Organization', + '@id': 'https://www.qrmaster.net/#organization', + name: 'QR Master', + alternateName: 'QRMaster', + url: 'https://www.qrmaster.net', + logo: { + '@type': 'ImageObject', + url: 'https://www.qrmaster.net/static/og-image.png', + width: 1200, + height: 630, + }, + image: 'https://www.qrmaster.net/static/og-image.png', + sameAs: [ + 'https://twitter.com/qrmaster', + ], + contactPoint: { + '@type': 'ContactPoint', + contactType: 'Customer Support', + email: 'support@qrmaster.net', + availableLanguage: ['English', 'German'], + }, + description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.', + slogan: 'Dynamic QR codes that work smarter', + foundingDate: '2025', + areaServed: 'Worldwide', + serviceType: 'Software as a Service', + priceRange: '$0 - $29', + knowsAbout: [ + 'QR Code Generation', + 'Marketing Analytics', + 'Campaign Tracking', + 'Dynamic QR Codes', + 'Bulk QR Generation', + ], + hasOfferCatalog: { + '@type': 'OfferCatalog', + name: 'QR Master Plans', + itemListElement: [ + { + '@type': 'Offer', + itemOffered: { + '@type': 'SoftwareApplication', + name: 'QR Master Free', + applicationCategory: 'BusinessApplication', + operatingSystem: 'Web Browser', + }, + }, + { + '@type': 'Offer', + itemOffered: { + '@type': 'SoftwareApplication', + name: 'QR Master Pro', + applicationCategory: 'BusinessApplication', + operatingSystem: 'Web Browser', + }, + }, + ], + }, + inLanguage: 'en', + mainEntityOfPage: 'https://www.qrmaster.net', + }; +} + +export function websiteSchema() { + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + '@id': 'https://www.qrmaster.net/#website', + name: 'QR Master', + url: 'https://www.qrmaster.net', + inLanguage: 'en', + mainEntityOfPage: 'https://www.qrmaster.net', + publisher: { + '@id': 'https://www.qrmaster.net/#organization', + }, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}', + }, + 'query-input': 'required name=search_term_string', + }, + }; +} + +export function breadcrumbSchema(items: BreadcrumbItem[]) { + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + '@id': `https://www.qrmaster.net${items[items.length - 1]?.url}#breadcrumb`, + inLanguage: 'en', + mainEntityOfPage: `https://www.qrmaster.net${items[items.length - 1]?.url}`, + itemListElement: items.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: `https://www.qrmaster.net${item.url}`, + })), + }; +} + +export function blogPostingSchema(post: BlogPost) { + return { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + '@id': `https://www.qrmaster.net/blog/${post.slug}#article`, + headline: post.title, + description: post.description, + image: post.image, + datePublished: post.datePublished, + dateModified: post.dateModified, + inLanguage: 'en', + mainEntityOfPage: `https://www.qrmaster.net/blog/${post.slug}`, + author: { + '@type': 'Person', + name: post.author, + url: post.authorUrl, + }, + publisher: { + '@type': 'Organization', + name: 'QR Master', + url: 'https://www.qrmaster.net', + logo: { + '@type': 'ImageObject', + url: 'https://www.qrmaster.net/static/og-image.png', + width: 1200, + height: 630, + }, + }, + isPartOf: { + '@type': 'Blog', + '@id': 'https://www.qrmaster.net/blog#blog', + name: 'QR Master Blog', + url: 'https://www.qrmaster.net/blog', + }, + }; +} + +export function faqPageSchema(faqs: FAQItem[]) { + return { + '@context': 'https://schema.org', + '@type': 'FAQPage', + '@id': 'https://www.qrmaster.net/faq#faqpage', + inLanguage: 'en', + mainEntityOfPage: 'https://www.qrmaster.net/faq', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: faq.answer, + }, + })), + }; +} + +export function productSchema(product: { name: string; description: string; offers: ProductOffer[] }) { + return { + '@context': 'https://schema.org', + '@type': 'Product', + '@id': 'https://www.qrmaster.net/pricing#product', + name: product.name, + description: product.description, + inLanguage: 'en', + mainEntityOfPage: 'https://www.qrmaster.net/pricing', + brand: { + '@type': 'Organization', + name: 'QR Master', + }, + offers: product.offers.map((offer) => ({ + '@type': 'Offer', + name: offer.name, + price: offer.price, + priceCurrency: offer.priceCurrency, + availability: offer.availability, + url: offer.url, + })), + }; +} + +export function howToSchema(task: HowToTask) { + return { + '@context': 'https://schema.org', + '@type': 'HowTo', + '@id': `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`, + name: task.name, + description: task.description, + inLanguage: 'en', + mainEntityOfPage: `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`, + totalTime: task.totalTime || 'PT5M', + step: task.steps.map((step, index) => ({ + '@type': 'HowToStep', + position: index + 1, + name: step.name, + text: step.text, + url: step.url, + })), + }; +} diff --git a/src/lib/validationSchemas.ts b/src/lib/validationSchemas.ts index 72c53ee..0bbaa46 100644 --- a/src/lib/validationSchemas.ts +++ b/src/lib/validationSchemas.ts @@ -1,186 +1,186 @@ -/** - * Zod Validation Schemas for API endpoints - * Centralized validation logic for type-safety and security - */ - -import { z } from 'zod'; - -// ========================================== -// QR Code Schemas -// ========================================== - -export const qrStyleSchema = z.object({ - fgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid foreground color format').optional(), - bgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid background color format').optional(), - cornerStyle: z.enum(['square', 'rounded']).optional(), - size: z.number().min(100).max(1000).optional(), -}); - -export const createQRSchema = z.object({ - title: z.string() - .min(1, 'Title is required') - .max(100, 'Title must be less than 100 characters'), - - content: z.record(z.any()), // Accept any object structure for content - - isStatic: z.boolean().optional(), - - contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'PDF', 'APP', 'COUPON', 'FEEDBACK'], { - errorMap: () => ({ message: 'Invalid content type' }) - }), - - tags: z.array(z.string()).optional(), - - style: z.object({ - foregroundColor: z.string().optional(), - backgroundColor: z.string().optional(), - cornerStyle: z.enum(['square', 'rounded']).optional(), - size: z.number().optional(), - }).optional(), -}); - -export const updateQRSchema = z.object({ - title: z.string() - .min(1, 'Title is required') - .max(100, 'Title must be less than 100 characters') - .optional(), - - content: z.string() - .min(1, 'Content is required') - .max(5000, 'Content must be less than 5000 characters') - .optional(), - - style: qrStyleSchema.optional(), - - isActive: z.boolean().optional(), -}); - -export const bulkQRSchema = z.object({ - qrs: z.array( - z.object({ - title: z.string().min(1).max(100), - content: z.string().min(1).max(5000), - contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'PDF', 'APP', 'COUPON', 'FEEDBACK']), - }) - ).min(1, 'At least one QR code is required') - .max(100, 'Maximum 100 QR codes per bulk creation'), -}); - -// ========================================== -// Authentication Schemas -// ========================================== - -export const loginSchema = z.object({ - email: z.string() - .email('Invalid email format') - .toLowerCase(), - - password: z.string() - .min(1, 'Password is required'), -}); - -export const signupSchema = z.object({ - name: z.string() - .min(2, 'Name must be at least 2 characters') - .max(100, 'Name must be less than 100 characters') - .trim(), - - email: z.string() - .email('Invalid email format') - .toLowerCase() - .trim(), - - password: z.string() - .min(8, 'Password must be at least 8 characters') - .max(100, 'Password must be less than 100 characters'), - // Password complexity rules removed for easier testing -}); - -export const forgotPasswordSchema = z.object({ - email: z.string() - .email('Invalid email format') - .toLowerCase() - .trim(), -}); - -export const resetPasswordSchema = z.object({ - token: z.string().min(1, 'Reset token is required'), - password: z.string() - .min(8, 'Password must be at least 8 characters') - .max(100, 'Password must be less than 100 characters'), - // Password complexity rules removed for easier testing -}); - -// ========================================== -// Settings Schemas -// ========================================== - -export const updateProfileSchema = z.object({ - name: z.string() - .min(2, 'Name must be at least 2 characters') - .max(100, 'Name must be less than 100 characters') - .trim(), -}); - -export const changePasswordSchema = z.object({ - currentPassword: z.string() - .min(1, 'Current password is required'), - - newPassword: z.string() - .min(8, 'Password must be at least 8 characters') - .max(100, 'Password must be less than 100 characters'), - // Password complexity rules removed for easier testing -}); - -// ========================================== -// Stripe Schemas -// ========================================== - -export const createCheckoutSchema = z.object({ - priceId: z.string().min(1, 'Price ID is required'), -}); - -// ========================================== -// Newsletter Schemas -// ========================================== - -export const newsletterSubscribeSchema = z.object({ - email: z.string() - .email('Invalid email format') - .toLowerCase() - .trim() - .max(255, 'Email must be less than 255 characters'), -}); - -// ========================================== -// Helper: Format Zod Errors -// ========================================== - -export function formatZodError(error: z.ZodError) { - return { - error: 'Validation failed', - details: error.errors.map(err => ({ - field: err.path.join('.'), - message: err.message, - })), - }; -} - -// ========================================== -// Helper: Validate with Zod -// ========================================== - -export async function validateRequest<T>( - schema: z.ZodSchema<T>, - data: unknown -): Promise<{ success: true; data: T } | { success: false; error: any }> { - try { - const validatedData = schema.parse(data); - return { success: true, data: validatedData }; - } catch (error) { - if (error instanceof z.ZodError) { - return { success: false, error: formatZodError(error) }; - } - return { success: false, error: { error: 'Invalid request data' } }; - } -} +/** + * Zod Validation Schemas for API endpoints + * Centralized validation logic for type-safety and security + */ + +import { z } from 'zod'; + +// ========================================== +// QR Code Schemas +// ========================================== + +export const qrStyleSchema = z.object({ + fgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid foreground color format').optional(), + bgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid background color format').optional(), + cornerStyle: z.enum(['square', 'rounded']).optional(), + size: z.number().min(100).max(1000).optional(), +}); + +export const createQRSchema = z.object({ + title: z.string() + .min(1, 'Title is required') + .max(100, 'Title must be less than 100 characters'), + + content: z.record(z.any()), // Accept any object structure for content + + isStatic: z.boolean().optional(), + + contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'PDF', 'APP', 'COUPON', 'FEEDBACK'], { + errorMap: () => ({ message: 'Invalid content type' }) + }), + + tags: z.array(z.string()).optional(), + + style: z.object({ + foregroundColor: z.string().optional(), + backgroundColor: z.string().optional(), + cornerStyle: z.enum(['square', 'rounded']).optional(), + size: z.number().optional(), + }).optional(), +}); + +export const updateQRSchema = z.object({ + title: z.string() + .min(1, 'Title is required') + .max(100, 'Title must be less than 100 characters') + .optional(), + + content: z.string() + .min(1, 'Content is required') + .max(5000, 'Content must be less than 5000 characters') + .optional(), + + style: qrStyleSchema.optional(), + + isActive: z.boolean().optional(), +}); + +export const bulkQRSchema = z.object({ + qrs: z.array( + z.object({ + title: z.string().min(1).max(100), + content: z.string().min(1).max(5000), + contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'PDF', 'APP', 'COUPON', 'FEEDBACK']), + }) + ).min(1, 'At least one QR code is required') + .max(100, 'Maximum 100 QR codes per bulk creation'), +}); + +// ========================================== +// Authentication Schemas +// ========================================== + +export const loginSchema = z.object({ + email: z.string() + .email('Invalid email format') + .toLowerCase(), + + password: z.string() + .min(1, 'Password is required'), +}); + +export const signupSchema = z.object({ + name: z.string() + .min(2, 'Name must be at least 2 characters') + .max(100, 'Name must be less than 100 characters') + .trim(), + + email: z.string() + .email('Invalid email format') + .toLowerCase() + .trim(), + + password: z.string() + .min(8, 'Password must be at least 8 characters') + .max(100, 'Password must be less than 100 characters'), + // Password complexity rules removed for easier testing +}); + +export const forgotPasswordSchema = z.object({ + email: z.string() + .email('Invalid email format') + .toLowerCase() + .trim(), +}); + +export const resetPasswordSchema = z.object({ + token: z.string().min(1, 'Reset token is required'), + password: z.string() + .min(8, 'Password must be at least 8 characters') + .max(100, 'Password must be less than 100 characters'), + // Password complexity rules removed for easier testing +}); + +// ========================================== +// Settings Schemas +// ========================================== + +export const updateProfileSchema = z.object({ + name: z.string() + .min(2, 'Name must be at least 2 characters') + .max(100, 'Name must be less than 100 characters') + .trim(), +}); + +export const changePasswordSchema = z.object({ + currentPassword: z.string() + .min(1, 'Current password is required'), + + newPassword: z.string() + .min(8, 'Password must be at least 8 characters') + .max(100, 'Password must be less than 100 characters'), + // Password complexity rules removed for easier testing +}); + +// ========================================== +// Stripe Schemas +// ========================================== + +export const createCheckoutSchema = z.object({ + priceId: z.string().min(1, 'Price ID is required'), +}); + +// ========================================== +// Newsletter Schemas +// ========================================== + +export const newsletterSubscribeSchema = z.object({ + email: z.string() + .email('Invalid email format') + .toLowerCase() + .trim() + .max(255, 'Email must be less than 255 characters'), +}); + +// ========================================== +// Helper: Format Zod Errors +// ========================================== + +export function formatZodError(error: z.ZodError) { + return { + error: 'Validation failed', + details: error.errors.map(err => ({ + field: err.path.join('.'), + message: err.message, + })), + }; +} + +// ========================================== +// Helper: Validate with Zod +// ========================================== + +export async function validateRequest<T>( + schema: z.ZodSchema<T>, + data: unknown +): Promise<{ success: true; data: T } | { success: false; error: any }> { + try { + const validatedData = schema.parse(data); + return { success: true, data: validatedData }; + } catch (error) { + if (error instanceof z.ZodError) { + return { success: false, error: formatZodError(error) }; + } + return { success: false, error: { error: 'Invalid request data' } }; + } +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 26b5d61..6e47e18 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -1,13 +1,13 @@ -import { DefaultSession } from 'next-auth'; - -declare module 'next-auth' { - interface Session { - user: { - id: string; - } & DefaultSession['user']; - } - - interface User { - id: string; - } +import { DefaultSession } from 'next-auth'; + +declare module 'next-auth' { + interface Session { + user: { + id: string; + } & DefaultSession['user']; + } + + interface User { + id: string; + } } \ No newline at end of file