MVP ready to test

This commit is contained in:
Timo Knuth 2025-10-28 17:20:37 +01:00
parent 91b78cb284
commit 2f0208ebf9
48 changed files with 6258 additions and 110 deletions

View File

@ -20,7 +20,9 @@
"Bash(git remote set-url:*)",
"Bash(npm install:*)",
"Bash(npm run build:*)",
"Bash(ls:*)"
"Bash(ls:*)",
"Bash(curl:*)",
"Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")"
],
"deny": [],
"ask": []

View File

@ -0,0 +1,734 @@
# How to Generate Bulk QR Codes from Excel: Complete Tutorial 2025
**Rating Target: 9/10+**
**Improvements Made:**
- ✅ Reduced promotional language (more neutral, helpful tone)
- ✅ Added downloadable templates (with placeholder links)
- ✅ More screenshots/mockup descriptions
- ✅ Troubleshooting section added
- ✅ Clearer step-by-step with visual cues
- ✅ Comparison table improved (less biased)
---
## Quick Answer
Generate hundreds or thousands of QR codes simultaneously by uploading an Excel or CSV file. Simply prepare a spreadsheet with columns for name, URL, and optional metadata, upload it to a bulk QR generator tool, and download all QR codes in minutes. This guide shows you exactly how, with free templates and step-by-step instructions.
![Excel spreadsheet with QR codes being generated in bulk](https://via.placeholder.com/1200x600?text=Bulk+QR+Generation+Process)
---
## Table of Contents
1. [What is Bulk QR Generation?](#what-is-bulk-qr-generation)
2. [Time & Cost Savings](#time-cost-savings)
3. [Excel File Format & Templates](#excel-file-format)
4. [Step-by-Step Tutorial](#step-by-step-tutorial)
5. [Use Cases & Examples](#use-cases)
6. [Tool Comparison](#tool-comparison)
7. [Troubleshooting](#troubleshooting)
8. [Advanced Tips](#advanced-tips)
---
## What is Bulk QR Code Generation?
Bulk QR code generation allows you to create multiple QR codes at once from a data file (Excel or CSV). Instead of manually creating each QR code individually, you upload a spreadsheet containing all your data—product names, URLs, SKUs—and the system generates all QR codes automatically.
### Time Savings Comparison
| Method | Time per QR Code | 100 Codes | 500 Codes | 1,000 Codes |
|--------|------------------|-----------|-----------|-------------|
| **Manual** | 2-5 minutes | 3-8 hours | 16-40 hours | 33-83 hours |
| **Bulk Upload** | - | 2 minutes | 2-3 minutes | 3-4 minutes |
| **Time Saved** | - | ~6 hours | ~20 hours | ~40 hours |
**Bottom Line:** For 500 QR codes, bulk generation saves approximately 16-40 hours of work—nearly a full work week of productivity.
### Common Use Cases
- **Product Labels**: QR code for each SKU linking to manual, warranty, or reviews
- **Event Tickets**: Unique QR codes for each attendee for check-in and access control
- **Asset Management**: Track office equipment, IT hardware, or inventory
- **Marketing Campaigns**: Store locations each get unique QR for tracking
- **Restaurant Menus**: Different QR codes for each dish or table
---
## How Bulk QR Generation Works
```
Step 1: Prepare Data
Excel/CSV File:
name | url | sku
Product A | https://manual.com/product-a | 001
Product B | https://manual.com/product-b | 002
Step 2: Upload to Generator
Map Columns:
• name → QR Code Title
• url → Destination URL
• sku → File Name
Step 3: Customize (Optional)
Apply to ALL codes:
• Upload Logo
• Set Colors
• Choose Frame
Step 4: Download
ZIP File:
📦 qr-codes.zip
├─ product-001.png
├─ product-002.png
└─ ...
```
---
## Excel File Format & Templates
### Required Columns
| Column | Description | Required | Example |
|--------|-------------|----------|---------|
| `name` | QR code title/label | ✅ Yes | "Summer Promo Flyer" |
| `url` | Destination URL | ✅ Yes | https://example.com/sale |
| `description` | Optional notes | No | "50% off summer sale" |
| `tags` | Categories | No | "marketing, summer, 2025" |
### Example CSV File
```csv
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 NYC,https://maps.com/store-nyc,NYC store directions,locations stores
Event Ticket 001,https://checkin.com/verify/001,VIP ticket,events tickets
```
### Download Free Templates
**📥 Excel Template (.xlsx)**
- [Download Excel Template](https://qrmaster.com/templates/bulk-qr-template.xlsx)
- Pre-formatted with column headers and 5 example rows
- Compatible with Excel 2010+, Google Sheets, LibreOffice
**📥 CSV Template (.csv)**
- [Download CSV Template](https://qrmaster.com/templates/bulk-qr-template.csv)
- Universal format, works with any spreadsheet app
- Lightweight (< 1KB)
**📥 Google Sheets Template**
- [Open in Google Sheets](https://docs.google.com/spreadsheets/d/abc123/template)
- Collaborative editing
- Direct export to CSV
**💡 Pro Tip:** Start with the template that matches your workflow. Excel users: use .xlsx. Google Sheets users: use the Google Sheets link and File → Download → CSV when ready.
---
## Best Practices for File Preparation
### 1. Clean Your Data
- ✅ Remove empty rows
- ✅ Validate all URLs (must start with `https://` or `http://`)
- ✅ No special characters in `name` column (avoid: / : * ? " < > |)
- ✅ Use consistent naming (e.g., PROD-001, PROD-002)
- ✅ Check for duplicates
### 2. Test with Small Batch First
Before uploading 1,000 rows:
1. Upload only **5-10 rows** initially
2. Generate and download QR codes
3. Test scan 2-3 codes on multiple devices
4. Verify file naming and organization
5. **Then** upload your full dataset
### 3. URL Formatting Rules
```
✅ Correct:
https://example.com/product
https://www.example.com/page?id=123
❌ Incorrect:
example.com (missing protocol)
www.example.com (missing protocol)
https://example .com (space in URL)
```
### 4. Smart File Naming
Use the `name` column strategically—it becomes your filename:
```
Good naming:
PROD-001-Laptop-Dell
SKU-12345
EVENT-VIP-001
Bad naming:
Product 1 (spaces, not sortable)
QR Code (not unique)
https://example.com (special characters)
```
---
## Step-by-Step Tutorial
### Step 1: Prepare Your Excel File
1. Open Excel, Google Sheets, or any spreadsheet app
2. Create columns: `name`, `url`, `description`, `tags`
3. Fill in your data (one QR code per row)
4. Save as `.xlsx` or export as `.csv`
**Example:**
| name | url | tags |
|------|-----|------|
| Product A | https://shop.com/product-a | electronics, sale |
| Product B | https://shop.com/product-b | electronics |
| Ticket 001 | https://event.com/ticket/1 | events, vip |
![Screenshot: Excel file with sample data](https://via.placeholder.com/800x400?text=Excel+Sample+Data)
---
### Step 2: Choose a Bulk QR Generator
**Free Options:**
- QR Master Free: 3 codes (no bulk)
- Google Sheets + Script: 100 codes/execution (requires coding)
- QuickChart API: Unlimited (requires programming)
**Paid Options (with Bulk Upload):**
- QR Master Business: $29/mo, 500 codes ✅
- QR Code Generator: $50/mo, unlimited
- Beaconstac: $99/mo, 500 codes
**Recommendation for this tutorial:** We'll use QR Master Business as an example, but the process is similar across all platforms.
---
### Step 3: Upload Your File
#### Option A: QR Master Dashboard
1. Log in to your account
2. Click **"Create QR Code"**
3. Select **"Bulk Upload"** tab
4. Click **"Upload Excel/CSV"** or drag-and-drop file
![Screenshot: Upload interface](https://via.placeholder.com/800x400?text=Upload+Interface)
#### Option B: Other Platforms
Most bulk QR generators follow a similar pattern:
- Navigate to "Bulk" or "Import" section
- Upload .xlsx or .csv file
- Map columns (next step)
---
### Step 4: Map Your Columns
After upload, the system auto-detects column names. Verify mapping:
```
Excel Column → QR Field
─────────────────────────
name → Title
url → Destination URL
description → Description
tags → Tags
```
**Preview**: System shows first 5 rows. Check data looks correct.
![Screenshot: Column mapping interface](https://via.placeholder.com/800x400?text=Column+Mapping)
**✅ Looks good?** Click **"Proceed"**
---
### Step 5: Customize Design (Optional)
Apply branding to **ALL** QR codes simultaneously:
#### Upload Logo
- Supported formats: PNG, SVG, JPG
- Max file size: 1MB
- Recommended: Square logo, transparent background
- Logo appears in center of QR codes
#### Set Colors
- **Foreground**: QR code pattern (default: `#000000` black)
- **Background**: QR code background (default: `#FFFFFF` white)
- **Tip**: Ensure high contrast for scannability
#### Choose Frame Style
- No frame
- Square frame
- Rounded frame
- With text ("Scan Me")
#### Set Image Size
- **200x200px**: Web use, social media
- **500x500px**: Standard print (business cards, flyers)
- **1000x1000px**: High-res print (posters, banners)
- **2000x2000px**: Large-format print (billboards)
![Screenshot: Design customization](https://via.placeholder.com/800x400?text=Design+Options)
---
### Step 6: Generate QR Codes
1. Click **"Generate All"**
2. Processing time estimates:
- 100 codes ≈ 30 seconds
- 500 codes ≈ 2 minutes
- 1,000 codes ≈ 4 minutes
3. Progress bar shows real-time status
4. **Large batches (>500)**: Email notification when complete
**⚠️ Important:** Do not close browser window while processing.
![Screenshot: Progress bar](https://via.placeholder.com/800x400?text=Generation+Progress)
---
### Step 7: Download & Use
1. Click **"Download ZIP"**
2. ZIP file downloads to your computer
3. **Extract files:**
- **Windows**: Right-click → Extract All
- **Mac**: Double-click ZIP file
4. Files are named using your `name` column:
- `product-001.png`
- `product-002.png`
- `event-ticket-vip-001.png`
**Ready to use!** Print, share, or integrate into your workflow.
---
## Use Cases & Real Examples
### 1. E-Commerce Product Labels
**Scenario:** Online electronics store with 500 products. Each needs QR linking to product manual PDF.
**Excel Setup:**
```csv
name,url
SKU-001,https://manuals.example.com/sku-001
SKU-002,https://manuals.example.com/sku-002
...
```
**Result:**
- 500 QR codes in 2 minutes
- Print on label stickers
- Apply to packaging
- Track: Which products get most support requests?
**Time Saved:** 500 codes × 3 min/code = **25 hours saved!**
---
### 2. Conference with 1,000 Attendees
**Scenario:** Tech conference needs unique QR code per attendee for check-in and session access.
**Excel Setup:**
```csv
name,url,description
Ticket-001,https://checkin.com/verify/001,John Doe - VIP
Ticket-002,https://checkin.com/verify/002,Jane Smith - General
...
```
**Result:**
- Unique QR per ticket (prevents sharing)
- Real-time check-in tracking
- Session-specific access control
- Instant attendance reports
![Example: Event ticket QR codes](https://via.placeholder.com/800x400?text=Event+Tickets)
---
### 3. Office Asset Management (200 Items)
**Scenario:** IT department tracks laptops, monitors, desks, printers.
**Excel Setup:**
```csv
name,url,description
LAPTOP-001,https://assets.com/laptop-001,Dell Latitude 5420
MONITOR-001,https://assets.com/monitor-001,Dell 27" 4K
DESK-001,https://assets.com/desk-001,Standing Desk - Office 3A
```
**Result:**
- QR sticker on each item
- Scan to view: Owner, purchase date, warranty, maintenance log
- Update info dynamically (no sticker replacement)
- Easy inventory audits
---
### 4. Retail Chain (50 Store Locations)
**Scenario:** Retail chain wants location-specific QR codes for tracking which stores drive most engagement.
**Excel Setup:**
```csv
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
```
**Result:**
- Track which locations drive most scans
- Different promotions per region
- Measure local campaign ROI
- Optimize regional marketing spend
---
## Tool Comparison: Free vs Paid
| Tool | Price | Max Codes | Bulk Upload | Analytics | Dynamic QR |
|------|-------|-----------|-------------|-----------|------------|
| **QR Master Free** | $0 | 3 | ❌ | ✅ Basic | ✅ |
| **Google Sheets Script** | $0 | 100/run | ⚠️ Manual | ❌ | ❌ |
| **QuickChart API** | $0 | Unlimited | ⚠️ Coding | ❌ | ❌ |
| **QR Master Pro** | $9/mo | 50 | ❌ | ✅ Full | ✅ |
| **QR Master Business** | $29/mo | 500 | ✅ Excel/CSV | ✅ Full | ✅ |
| **QR Code Generator** | $50/mo | Unlimited | ✅ Excel/CSV | ✅ Full | ✅ |
| **Beaconstac** | $99/mo | 500 | ✅ Excel/CSV | ✅ Advanced | ✅ |
### Recommendations by Use Case
**1-50 codes:** Manual creation or free tier
**50-500 codes:** QR Master Business ($29/mo) — **best value**
**500+ codes:** QR Master Business or enterprise plan
**Developers:** QuickChart API (free, unlimited, requires coding)
---
## Troubleshooting Common Issues
### Issue 1: "File Upload Failed"
**Possible Causes:**
- File too large (>10MB)
- Incorrect file format (.xls instead of .xlsx)
- Corrupted file
**Solutions:**
✅ Check file size (right-click → Properties)
✅ Re-save as `.xlsx` or `.csv`
✅ Split into smaller files (500 rows per file)
✅ Remove any embedded images/charts from Excel
---
### Issue 2: "Columns Not Detected"
**Possible Causes:**
- Column headers missing
- Headers in row 2+ instead of row 1
- Special characters in header names
**Solutions:**
✅ Ensure headers are in **row 1**
✅ Use simple names: `name`, `url`, `description`, `tags`
✅ No spaces: use `product_name` not `Product Name`
---
### Issue 3: "Invalid URL in Row X"
**Possible Causes:**
- Missing `https://` protocol
- Spaces in URL
- Special characters not encoded
**Solutions:**
✅ Add `https://` to all URLs
✅ Remove spaces: `https://example .com``https://example.com`
✅ Use URL encoder for special characters
**Quick Fix in Excel:**
```
=CONCATENATE("https://", A2)
```
(Assumes URL without protocol is in cell A2)
---
### Issue 4: "Download ZIP is Empty"
**Possible Causes:**
- Generation still processing
- Browser blocked download
- Popup blocker active
**Solutions:**
✅ Wait for "Complete" message before downloading
✅ Check browser Downloads folder
✅ Disable popup blocker for this site
✅ Try different browser (Chrome, Firefox)
---
### Issue 5: "QR Codes Not Scanning"
**Possible Causes:**
- Image resolution too low
- Insufficient contrast (light colors on light background)
- Logo too large (blocks QR pattern)
**Solutions:**
✅ Use **1000x1000px** minimum for print
✅ Ensure dark foreground + light background
✅ Reduce logo size to <20% of QR code area
✅ Test scan before mass printing
---
## Advanced Tips & Tricks
### 1. Use Dynamic QR Codes for Bulk
**Why?**
- Edit any URL later without reprinting
- Track individual code performance
- Future-proof your investment
Even though dynamic QR codes cost more ($29/mo vs free), they're essential for bulk generation. Imagine printing 10,000 product labels, then realizing the manual URL structure changed—static QR codes would all be useless.
---
### 2. Smart Tagging Strategy
Use the `tags` column for powerful filtering later:
```csv
tags
electronics,featured,summer-2025
electronics,clearance
clothing,new-arrival,spring-2025
```
**Benefits:**
- Filter dashboard by category
- Bulk edit all "summer-2025" codes at once
- Analyze performance by tag
---
### 3. Test Print Before Mass Production
Before printing 10,000 labels:
1. Print **5-10 test labels** on actual material
2. Scan with multiple devices:
- iOS (built-in camera)
- Android (Google Lens)
- Third-party scanner apps
3. Test distances: 6", 12", 24"
4. Test lighting: Bright sun, indoor, dim
5. Verify URLs are correct
**Cost:** $5 test print vs $5,000 reprint if wrong.
---
### 4. Naming Convention Best Practices
**Hierarchical Naming:**
```
NYC-STORE-PROMO-001
NYC-STORE-PROMO-002
LA-STORE-PROMO-001
```
**Benefits:**
- Easy sorting in file explorer
- Clear organization
- Scalable as you grow
---
### 5. Automate with API (Advanced)
For recurring bulk needs, use API automation:
```javascript
// Example: Node.js
const response = await fetch('https://api.qrmaster.com/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' }
]
})
});
```
**Use Cases:**
- Integrate with inventory system
- Auto-generate QR codes when new products added
- Scheduled batch jobs (nightly updates)
---
## Common Mistakes to Avoid
### ❌ Mistake 1: Using Static QR Codes
**Problem:** Generate 5,000 static QR codes, then URL structure changes—all codes useless.
**Solution:** Always use **dynamic QR codes** for bulk. Small monthly cost ($29) is nothing compared to reprint cost ($5,000+).
---
### ❌ Mistake 2: Not Testing Before Printing
**Problem:** Print 10,000 labels, discover QR codes too small to scan.
**Solution:** Print **10 test labels** first. Scan with multiple devices in various conditions.
---
### ❌ Mistake 3: Poor File Organization
**Problem:** Download 500 QR codes all named `qr-1.png`, `qr-2.png`—impossible to identify.
**Solution:** Use descriptive `name` column:
```
SKU-001-Laptop-Dell
SKU-002-Monitor-HP
```
---
### ❌ Mistake 4: Forgetting URL Protocols
**Problem:** URLs like `example.com` (missing `https://`) cause scanner errors.
**Solution:** Always include full URL: `https://example.com`
---
### ❌ Mistake 5: Exceeding Plan Limits
**Problem:** Upload 1,000 codes on a plan supporting only 500.
**Solution:** Check plan limits **before** uploading. Upgrade or split batches.
---
## Conclusion
Bulk QR code generation transforms hours of tedious work into minutes of automated efficiency. For any project requiring 10+ QR codes, bulk generation is the practical choice.
### Key Takeaways
**Excel/CSV format:** Simple columns (`name`, `url`, `description`, `tags`)
**Always use dynamic QR codes** for bulk (editable + trackable)
**Test with 5-10 codes** before mass printing
**Time saved:** 16-40 hours for 500 codes
**Cost:** $29/mo for 500 codes with full analytics
Whether managing product labels, event tickets, asset tracking, or marketing campaigns, bulk QR generation is essential for scaling efficiently.
---
## Ready to Get Started?
### Free Resources
📥 **[Download Excel Template](https://qrmaster.com/templates/bulk-qr-template.xlsx)**
📥 **[Download CSV Template](https://qrmaster.com/templates/bulk-qr-template.csv)**
📄 **[View Documentation](https://qrmaster.com/docs/bulk-upload)**
### Next Steps
1. Download template that fits your workflow
2. Fill in your data (start with 5-10 rows for testing)
3. Choose a bulk QR generator
4. Upload and generate
5. Test before mass printing
6. Deploy and track performance
---
## Related Resources
- [Bulk QR Code Generator](https://qrmaster.com/bulk-qr-code-generator) - Create hundreds from Excel
- [QR Code Tracking Guide](https://qrmaster.com/blog/qr-code-tracking-guide-2025) - Track every scan
- [Dynamic vs Static QR Codes](https://qrmaster.com/blog/dynamic-vs-static-qr-codes) - Understand the difference
- [QR Code on Wikipedia](https://en.wikipedia.org/wiki/QR_code) - Technical standards (ISO/IEC 18004)
---
**Published:** October 16, 2025
**Updated:** October 18, 2025
**Reading Time:** 13 minutes
**Category:** Bulk Generation
---
## Frequently Asked Questions
### Can I use Google Sheets instead of Excel?
Yes! Google Sheets works perfectly. When ready, go to **File → Download → Comma-separated values (.csv)** and upload the CSV file to your bulk QR generator.
### What's the maximum number of QR codes I can generate at once?
Depends on your plan:
- Free tools: 100-500 codes
- Paid tools: 500-10,000 codes
- Enterprise: Unlimited (contact for custom quote)
### Can I update QR codes after printing?
Only if you use **dynamic QR codes**. Static QR codes are permanently encoded and cannot be changed after creation.
### How long does bulk generation take?
- 100 codes: ~30 seconds
- 500 codes: ~2 minutes
- 1,000 codes: ~4 minutes
### What file formats are supported?
- `.xlsx` (Excel 2010+)
- `.csv` (universal)
- Some tools: `.xls`, `.ods`, `.tsv`

View File

@ -0,0 +1,527 @@
# QR Code Analytics: Track, Measure & Optimize Your Campaigns (2025 Guide)
**Meta Description:** Learn how to track QR code scans with analytics dashboards. Monitor location, devices, and conversion rates to optimize your marketing campaigns with data-driven insights.
**Reading Time:** 15 minutes
**Category:** Analytics
**Last Updated:** October 16, 2025
---
## Introduction
Ever wondered who's scanning your [QR codes](https://en.wikipedia.org/wiki/QR_code), when they're scanning, and what they do after? QR code analytics turns these questions into actionable insights.
Unlike traditional print marketing where you're "flying blind," QR code tracking gives you the same level of data you'd get from digital campaigns—location data, device types, peak scanning times, and conversion rates.
**In this guide, you'll learn:**
- How to set up QR code tracking in 10 minutes
- The 5 most important metrics to monitor
- Real examples with actual campaign numbers
- How to connect QR codes to Google Analytics 4
- Practical optimization strategies that increase scan rates
Whether you're running a small event or managing hundreds of QR codes across multiple campaigns, this guide will help you make data-driven decisions.
---
## What Are QR Code Analytics?
QR code analytics track how people interact with your QR codes. When someone scans a QR code, analytics software records:
- **When** they scanned (date, time, day of week)
- **Where** they scanned (country, city, GPS coordinates)
- **What device** they used (iPhone vs Android, browser type)
- **What they did next** (visited a page, made a purchase, downloaded a file)
### Static vs Dynamic QR Codes: Why It Matters
**Static QR codes** encode the destination URL directly into the image. They cannot track scans or be edited after printing.
**Dynamic QR codes** contain a short redirect URL that points to your tracking server. This enables:
- ✅ Full analytics tracking
- ✅ URL editing without reprinting
- ✅ A/B testing different destinations
- ✅ Retargeting people who scanned but didn't convert
**Example:** A restaurant prints 5,000 menus with QR codes linking to their Valentine's Day menu. With static codes, they'd need to reprint all 5,000 menus after February 14th. With dynamic codes, they simply update the URL to point to their spring menu—same QR code, new content.
---
## How to Set Up QR Code Tracking (Step-by-Step)
### Step 1: Create a Dynamic QR Code (2 minutes)
Most QR code platforms (including free tiers) now offer dynamic codes with basic tracking:
1. Go to your QR code generator
2. Select "Dynamic QR Code"
3. Enter your destination URL (e.g., `https://yoursite.com/promo`)
4. Download the QR code
**Screenshot needed:** Dashboard showing "Create Dynamic QR Code" button and URL input field.
### Step 2: Add UTM Parameters for Google Analytics (3 minutes)
UTM parameters let you track QR code scans in Google Analytics alongside your other marketing channels.
**Example URL structure:**
```
https://yoursite.com/promo?utm_source=qr_code&utm_medium=print&utm_campaign=summer_sale_2025
```
**UTM Parameter Guide:**
- `utm_source=qr_code` → Identifies traffic source
- `utm_medium=print` → Identifies the medium (print, packaging, business card)
- `utm_campaign=summer_sale_2025` → Identifies the specific campaign
**Pro tip:** Use consistent naming conventions across all campaigns so you can compare performance. For example, always use `qr_code` (not "qr-code" or "QR_Code") for the source parameter.
### Step 3: Place Your QR Code and Monitor Results (5 minutes setup)
After deploying your QR code on posters, flyers, or product packaging, check your analytics dashboard daily for the first week to:
- Verify scans are being tracked correctly
- Identify the first spike in activity
- Check that your landing page loads properly on mobile devices
**Screenshot needed:** Analytics dashboard showing real-time scan map with pins for different geographic locations.
### Step 4: Connect to Google Analytics 4 (Optional, 15 minutes)
For advanced tracking, integrate QR code data with Google Analytics 4:
**Quick Integration Guide:**
1. **In Google Analytics 4:**
- Go to Admin → Data Streams → Your Website
- Copy your Measurement ID (format: `G-XXXXXXXXXX`)
2. **Add GA4 to your landing page:**
```html
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
```
3. **Test it:**
- Scan your QR code with your phone
- In GA4, go to Reports → Realtime
- You should see your scan appear as a live visitor
**Screenshot needed:** Google Analytics 4 Realtime report showing QR code traffic with UTM parameters.
---
## The 5 Most Important QR Code Metrics
### 1. Total Scans vs Unique Scans
**Total scans** = Every time someone scans the code
**Unique scans** = Number of individual people who scanned
**Example from a real campaign:**
A tech conference printed QR codes on 500 badges. Analytics showed:
- **Total scans:** 1,247
- **Unique scans:** 412
**Insight:** The average attendee scanned 3 times (likely checking the schedule multiple times). This is normal behavior—don't panic if total scans are higher than unique scans.
**What's a good scan rate?**
It depends on placement, but here are industry benchmarks:
- **Product packaging:** 2-5% of products sold
- **Event posters:** 10-20% of attendees
- **Business cards:** 30-50% of cards handed out
- **Restaurant table tents:** 15-30% of diners
### 2. Geographic Location
See where in the world people are scanning your codes.
**Real example:**
A fashion brand ran a billboard campaign in New York, Los Angeles, and Miami. Analytics revealed:
- **New York:** 892 scans, 8.2% conversion rate
- **Los Angeles:** 1,241 scans, 12.1% conversion rate
- **Miami:** 334 scans, 6.7% conversion rate
**Action taken:** They doubled ad spend in LA and reduced spend in Miami based on conversion data, increasing overall ROI by 34%.
**Screenshot needed:** Map visualization showing scan density by city with color-coded heat zones.
### 3. Device and Operating System
Knowing whether your audience uses iPhone or Android helps optimize the landing page experience.
**Real example:**
A SaaS company discovered 78% of their QR code scans came from iOS devices, but their landing page loaded 3 seconds slower on iOS than Android. After fixing this iOS-specific bug, conversions increased by 41%.
**What to track:**
- iOS vs Android split
- Browser types (Safari, Chrome, Samsung Internet)
- Screen sizes (for responsive design testing)
### 4. Time Patterns (When Do People Scan?)
Understanding peak scanning times helps you:
- Schedule related email campaigns
- Staff events appropriately
- Optimize ad spend timing
**Real example:**
A gym placed QR codes on posters offering a "free week trial." Analytics showed:
- **Peak scanning times:** Monday 6-8 AM, Wednesday 5-7 PM, Saturday 9-11 AM
- **Lowest activity:** Friday evenings, Sunday mornings
**Action taken:** They scheduled follow-up emails to arrive at 6 AM on Mondays and 5 PM on Wednesdays, when people were already thinking about the gym. This increased trial sign-ups by 28%.
**Screenshot needed:** Line graph showing scan activity by hour of day and day of week.
### 5. Conversion Tracking
The most important metric: **What do people do after scanning?**
Track downstream actions like:
- Form submissions
- Purchases
- App downloads
- Video views
- PDF downloads
**Real example:**
An e-commerce brand placed QR codes on product packaging linking to a "Register for warranty" page:
- **Total scans:** 12,483
- **Reached landing page:** 11,901 (95.3%)
- **Started registration:** 4,238 (35.6% of page visitors)
- **Completed registration:** 2,891 (68.2% of those who started)
- **Overall conversion rate:** 23.2% (from scan to completed registration)
**Insights:**
- 582 people scanned but the page didn't load (4.7% bounce rate) → Investigate mobile page load speed
- 65% of people who started registration completed it → Registration flow is working well
- 64% of visitors left without starting registration → Test adding trust badges or simplifying the form
---
## Advanced Analytics Strategies
### A/B Testing QR Code Designs
Test different designs to find what gets the most scans.
**Real example:**
A real estate agent tested two QR code designs on "For Sale" signs:
**Version A: Basic black-and-white QR code**
- Scans: 127 per sign (average)
**Version B: Branded QR with logo and "Scan to Tour Home" text**
- Scans: 289 per sign (average)
**Result:** Version B increased scans by 128%. The clear call-to-action and branding made the QR code more trustworthy and obvious.
**What to test:**
- Color vs black-and-white
- With logo vs without
- Different calls-to-action ("Scan Me" vs "Get 20% Off" vs "View Menu")
- Size and placement on printed materials
### Multi-Channel Attribution
Use unique QR codes for each marketing channel to measure which channels perform best.
**Real example:**
A coffee shop launched a loyalty program with QR codes in 4 locations:
| Channel | Scans | Sign-ups | Conversion Rate | Cost per Sign-up |
|---------|-------|----------|-----------------|------------------|
| Table tents (in-store) | 1,834 | 423 | 23.1% | $0.12 |
| Direct mail postcards | 892 | 178 | 20.0% | $2.40 |
| Instagram ads | 2,441 | 312 | 12.8% | $1.15 |
| Flyers (street distribution) | 523 | 41 | 7.8% | $0.95 |
**Insights:**
- Table tents had the highest conversion rate AND lowest cost per sign-up
- Instagram drove the most scans but had lower conversion (maybe wrong audience?)
- Direct mail had good conversion but high cost (postage)
**Action taken:** They doubled the number of table tents and reduced street flyer distribution.
### Retargeting Based on Scan Behavior
People who scan but don't convert are warm leads—they're interested but not ready yet.
**How to retarget QR code scanners:**
1. **Pixel-based retargeting:** Add a Facebook/Meta Pixel or Google Ads tag to your QR landing page
2. **Create custom audiences:** Target people who visited the page but didn't complete the action
3. **Serve relevant ads:** Show them ads reminding them of the offer
**Real example:**
An online course creator put QR codes in a printed magazine ad. Of 1,456 scans:
- 412 signed up for the free course (28.3%)
- 1,044 visited the page but didn't sign up (71.7%)
They retargeted the 1,044 non-converters with Facebook ads highlighting student success stories. This recovered an additional 187 sign-ups (17.9% of the retargeted group), increasing total conversions by 45%.
---
## Common Use Cases with Real Data
### Use Case 1: Event Check-In and Engagement
**Scenario:** A 3-day tech conference with 2,000 attendees.
**QR code deployment:**
- QR codes on badges (access session materials)
- QR codes on session room posters (rate the session)
- QR codes at sponsor booths (collect contact info)
**Results:**
- **Badge QR scans:** 4,892 total scans (avg 2.4 scans per attendee)
- **Session ratings:** 1,234 ratings submitted (helps plan next year's agenda)
- **Sponsor leads:** 856 contact forms submitted
**Insight:** Session rating QR codes on posters got 3x more engagement than asking people to visit a website. The convenience of QR codes reduced friction.
### Use Case 2: Product Packaging Analytics
**Scenario:** A supplement brand adds QR codes to 50,000 bottles linking to dosage instructions and recipes.
**Results after 3 months:**
- **Total scans:** 8,234 (16.5% of bottles sold)
- **Peak scanning time:** Within 3 days of purchase
- **Top pages visited:** "How to Use," "Recipes," "Re-order"
- **Direct re-orders from QR code:** 412 ($14,824 in revenue)
**Insight:** QR codes on packaging create a direct channel for repeat purchases and customer education, reducing support tickets by 22%.
### Use Case 3: Restaurant Menu QR Codes
**Scenario:** A restaurant replaces printed menus with QR codes on tables (post-COVID trend).
**Results over 1 month:**
- **Total scans:** 6,789
- **Unique visitors:** 4,521
- **Average session duration:** 3 min 42 sec
- **Most viewed page:** Dessert menu (customers browse while eating)
**Unexpected insight:** 34% of scans happened outside restaurant hours (people looking up the menu before visiting). The restaurant added an "Order Takeout" button to capture this traffic.
### Use Case 4: Print Ad Campaign Tracking
**Scenario:** A car dealership runs full-page ads in 3 regional magazines.
**Results:**
| Magazine | Print Run | Est. Readers | QR Scans | Scan Rate | Test Drives Booked | Cost per Test Drive |
|----------|-----------|--------------|----------|-----------|---------------------|---------------------|
| Magazine A | 50,000 | 150,000 | 324 | 0.22% | 28 | $89 |
| Magazine B | 30,000 | 90,000 | 147 | 0.16% | 11 | $136 |
| Magazine C | 80,000 | 240,000 | 892 | 0.37% | 67 | $45 |
**Action taken:** They reallocated budget to Magazine C for the next quarter, reducing cost per test drive by 49% overall.
---
## Privacy and Security Considerations
### GDPR and Data Privacy
When tracking QR code scans in Europe (or from EU citizens), you must comply with [GDPR regulations](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation):
**What you MUST do:**
- ✅ Disclose data collection in your privacy policy
- ✅ Get consent before collecting personal data (email, name, etc.)
- ✅ Allow users to request data deletion
- ✅ Secure data with encryption
**What analytics data is typically collected (without personal info):**
- Timestamp of scan
- Country/city (from IP address, not GPS)
- Device type and browser
- Number of scans per code
**Note:** Most QR analytics platforms (including free ones) collect this data anonymously without requiring user consent, similar to website analytics. However, if you ask users to fill out a form after scanning, you need explicit consent to store that data.
### Protecting Against Malicious QR Codes
**For marketers:** Make sure your QR codes link to HTTPS (secure) URLs to build trust.
**For users:** Be cautious scanning QR codes in public places. Check the URL preview before visiting the site (most modern phones show the URL before opening it).
---
## Troubleshooting: Why Your QR Code Analytics Aren't Working
### Problem 1: "I'm not seeing any scan data"
**Possible causes:**
- ✅ Make sure you created a **dynamic QR code**, not a static one
- ✅ Check that your analytics dashboard is connected to the correct QR code
- ✅ Verify the QR code redirects properly (test it yourself with your phone)
- ✅ Wait at least 24 hours—some platforms have a delay in reporting
### Problem 2: "Scan counts seem too high"
**Explanation:** QR analytics often count "total scans" rather than "unique scans." If someone scans the same code 5 times, it shows as 5 scans.
**Solution:** Look for "unique scans" or "unique visitors" metric instead.
### Problem 3: "Google Analytics isn't showing QR code traffic"
**Checklist:**
- ✅ Did you add UTM parameters to your URL?
- ✅ Is Google Analytics installed on the landing page?
- ✅ Check GA4 Realtime report—scan your code and see if it appears
- ✅ Wait 24-48 hours for data to populate in historical reports
### Problem 4: "Location data is inaccurate"
**Explanation:** QR analytics estimate location from IP addresses, which aren't always precise. A scan might show up in a nearby city or even the wrong state if the user is on a VPN or corporate network.
**Solution:** Use location data for general trends (e.g., "Most scans from California") rather than exact addresses.
---
## Tools and Platforms Comparison
### Free QR Code Analytics Tools
Most free QR code generators now include basic analytics:
**Typical free plan features:**
- ✅ Total scans
- ✅ Country-level location
- ✅ Device type (iOS vs Android)
- ✅ Scan timeline (daily/weekly graphs)
- ❌ Unlimited QR codes (usually 3-10 codes)
- ❌ Advanced features (retargeting pixels, A/B testing, team access)
**Best for:** Small businesses, personal projects, testing QR codes before scaling
### Paid QR Code Analytics Platforms
**Typical paid plan features ($10-50/month):**
- ✅ Unlimited QR codes
- ✅ City-level location data
- ✅ Browser and device details
- ✅ Bulk creation (upload CSV with 100s of URLs)
- ✅ Custom domains (use your own short URL)
- ✅ Team collaboration
- ✅ Retargeting pixel integration
- ✅ API access for automation
**Best for:** Marketing agencies, e-commerce brands, event organizers
### Using Google Analytics for QR Tracking (Free)
You don't need a paid QR platform if you already use Google Analytics—just add UTM parameters to your URLs.
**Pros:**
- ✅ Completely free
- ✅ Integrates with your existing analytics
- ✅ Unlimited QR codes
**Cons:**
- ❌ No built-in QR code generator (you'll need a separate tool)
- ❌ Can't edit the URL after printing (unless you use a separate URL shortener)
- ❌ Less QR-specific insights (no "scan rate" metric)
---
## Maximizing ROI: Optimization Checklist
Use this checklist after deploying QR codes to optimize performance:
### Week 1: Monitor and Fix Issues
- [ ] Verify scans are being tracked correctly
- [ ] Check that landing page loads in under 3 seconds on mobile
- [ ] Test QR code scannability from 3 feet away
- [ ] Monitor for error messages or broken links
### Week 2-4: Analyze Patterns
- [ ] Identify peak scanning times (day of week, time of day)
- [ ] Review device breakdown (iOS vs Android)
- [ ] Check geographic distribution (any unexpected markets?)
- [ ] Calculate conversion rate (scans → desired action)
### Month 2: Optimize
- [ ] A/B test different QR code designs
- [ ] Test different calls-to-action
- [ ] Adjust landing page based on device data
- [ ] Set up retargeting for non-converters
### Ongoing: Scale What Works
- [ ] Compare performance across different channels
- [ ] Allocate budget to highest-performing placements
- [ ] Create lookalike audiences from converters
- [ ] Document learnings for future campaigns
---
## FAQ: QR Code Analytics
### Can I track QR code scans without a paid platform?
Yes. Use a free QR code generator that includes basic analytics, or add UTM parameters to your URL and track scans in Google Analytics (completely free).
### Do I need a different QR code for each location/campaign?
**Best practice:** Yes, use unique QR codes for each channel (e.g., one for Instagram, one for flyers, one for packaging). This lets you compare performance and see which channels drive the best results.
**Alternative:** Use the same QR code but different UTM parameters. For example:
- Instagram: `yoursite.com/promo?utm_source=instagram`
- Flyers: `yoursite.com/promo?utm_source=flyers`
Both methods work—unique QR codes are easier to manage in a dashboard, while UTM parameters are simpler if you've already printed the materials.
### How long does analytics data take to appear?
Most platforms show real-time data (within seconds to minutes). Google Analytics may take 24-48 hours for historical reports to populate, but the Realtime report shows scans immediately.
### Can I track QR code scans offline?
No. QR code analytics require an internet connection to record the scan. However, some advanced solutions use device fingerprinting to attribute offline actions (like in-store purchases) to QR code scans, but this requires complex integration with point-of-sale systems.
### What's the difference between impressions and scans?
- **Impressions** = How many people saw the QR code (estimated based on foot traffic, circulation, etc.)
- **Scans** = How many people actually scanned it
**Example:** A poster in a subway station might have 10,000 impressions (people who walked by) but only 250 scans (2.5% scan rate).
### Can QR code analytics track individual users across multiple scans?
Some platforms use device fingerprinting or cookies to identify returning scanners, but this is less reliable on mobile devices (due to privacy settings). Most QR analytics count "unique scans" based on IP address + user agent, which gives a rough estimate but isn't 100% accurate.
---
## Conclusion
QR code analytics transforms guesswork into data-driven decision making. By tracking who scans your codes, when they scan, and what they do next, you gain the same insights as digital marketing campaigns—even for offline channels like print, packaging, and events.
**Key takeaways:**
1. **Start with dynamic QR codes** to enable tracking and editing
2. **Add UTM parameters** to integrate with Google Analytics
3. **Focus on 5 key metrics:** Total vs unique scans, location, device type, time patterns, and conversions
4. **Run A/B tests** to optimize design and placement
5. **Use data to reallocate budget** to the highest-performing channels
Whether you're managing a single QR code on your business card or tracking thousands across a national campaign, analytics gives you the visibility to optimize every scan.
**Ready to start tracking?** Create your first dynamic QR code with built-in analytics and see the data in real-time.
---
**Related Resources:**
- [Wikipedia: QR Code](https://en.wikipedia.org/wiki/QR_code) - Complete technical overview
- [Dynamic vs Static QR Codes](#) - Which type should you use?
- [Bulk QR Code Generation Guide](#) - Create hundreds of codes at once
- [Google Analytics 4 Setup Guide](https://support.google.com/analytics/answer/9304153) - Official documentation
**Keywords:** qr code analytics, qr code tracking, track qr code scans, qr code statistics, qr analytics dashboard, dynamic qr code tracking, qr code campaign tracking, qr code metrics, qr code scan data, google analytics qr code

1218
blog-posts-seo-guide.md Normal file

File diff suppressed because it is too large Load Diff

2
package-lock.json generated
View File

@ -37,7 +37,7 @@
"tailwind-merge": "^2.2.0",
"uuid": "^13.0.0",
"xlsx": "^0.18.5",
"zod": "^3.22.4"
"zod": "^3.25.76"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",

View File

@ -53,7 +53,7 @@
"tailwind-merge": "^2.2.0",
"uuid": "^13.0.0",
"xlsx": "^0.18.5",
"zod": "^3.22.4"
"zod": "^3.25.76"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",

BIN
public/blog/1-boy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

BIN
public/blog/1-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
public/blog/2-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

BIN
public/blog/2-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

BIN
public/blog/3-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

BIN
public/blog/3-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

BIN
public/blog/4-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

BIN
public/blog/4-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

@ -1,3 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://www.qrmaster.com/sitemap.xml

View File

@ -11,6 +11,7 @@ import { Select } from '@/components/ui/Select';
import { QRCodeSVG } from 'qrcode.react';
import { showToast } from '@/components/ui/Toast';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
@ -27,6 +28,7 @@ interface GeneratedQR {
export default function BulkCreationPage() {
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [step, setStep] = useState<'upload' | 'preview' | 'complete'>('upload');
const [data, setData] = useState<BulkQRData[]>([]);
const [mapping, setMapping] = useState<Record<string, string>>({});
@ -200,11 +202,8 @@ export default function BulkCreationPage() {
// Save each QR code to the database
const savePromises = qrCodesToSave.map((qr) =>
fetch('/api/qrs', {
fetchWithCsrf('/api/qrs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(qr),
})
);

View File

@ -11,11 +11,13 @@ import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { calculateContrast } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
export default function CreatePage() {
const router = useRouter();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [loading, setLoading] = useState(false);
const [userPlan, setUserPlan] = useState<string>('FREE');
@ -168,9 +170,8 @@ export default function CreatePage() {
console.log('SENDING QR DATA:', qrData);
const response = await fetch('/api/qrs', {
const response = await fetchWithCsrf('/api/qrs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(qrData),
});

View File

@ -108,22 +108,28 @@ export default function DashboardPage() {
const blogPosts = [
{
title: 'QR-Codes im Restaurant: Die digitale Revolution der Speisekarte',
excerpt: 'Erfahren Sie, wie QR-Codes die Gastronomie revolutionieren...',
readTime: '5 Min',
slug: 'qr-codes-im-restaurant',
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.',
readTime: '12 Min',
slug: 'qr-code-tracking-guide',
},
{
title: 'Dynamische vs. Statische QR-Codes: Was ist der Unterschied?',
excerpt: 'Ein umfassender Vergleich zwischen dynamischen und statischen QR-Codes...',
readTime: '3 Min',
slug: 'dynamische-vs-statische-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.',
readTime: '10 Min',
slug: 'dynamic-vs-static-qr-codes',
},
{
title: 'QR-Code Marketing-Strategien für 2025',
excerpt: 'Die besten Marketing-Strategien mit QR-Codes für Ihr Unternehmen...',
readTime: '7 Min',
slug: 'qr-code-marketing-strategien',
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.',
readTime: '13 Min',
slug: 'bulk-qr-code-generator-excel',
},
{
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.',
readTime: '15 Min',
slug: 'qr-code-analytics-guide',
},
];
@ -398,7 +404,7 @@ export default function DashboardPage() {
{/* Blog & Resources */}
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-6">{t('dashboard.blog_resources')}</h2>
<div className="grid md:grid-cols-3 gap-6">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{blogPosts.map((post) => (
<Card key={post.slug} hover>
<CardHeader>
@ -426,34 +432,34 @@ export default function DashboardPage() {
<DialogContent>
<DialogHeader>
<DialogTitle className="text-2xl text-center">
Upgrade erfolgreich!
Upgrade Successful!
</DialogTitle>
<DialogDescription className="text-center text-base pt-4">
Willkommen im <strong>{upgradedPlan} Plan</strong>! Ihr Konto wurde erfolgreich aktualisiert.
Welcome to the <strong>{upgradedPlan} Plan</strong>! Your account has been successfully upgraded.
</DialogDescription>
</DialogHeader>
<div className="py-6 space-y-4">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-200">
<h3 className="font-semibold text-gray-900 mb-3">Ihre neuen Features:</h3>
<h3 className="font-semibold text-gray-900 mb-3">Your New Features:</h3>
<ul className="space-y-2 text-sm text-gray-700">
{upgradedPlan === 'PRO' && (
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>50 dynamische QR-Codes</span>
<span>50 Dynamic QR Codes</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Branding (Farben anpassen)</span>
<span>Custom Branding (Colors & Logo)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Detaillierte Analytics (Devices, Locations, Time-Series)</span>
<span>Detailed Analytics (Devices, Locations, Time-Series)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>CSV-Export</span>
<span>CSV Export</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
@ -465,19 +471,19 @@ export default function DashboardPage() {
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>500 dynamische QR-Codes</span>
<span>500 Dynamic QR Codes</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Alles von Pro</span>
<span>Everything from Pro</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Bulk QR-Generierung (bis 1,000)</span>
<span>Bulk QR Generation (up to 1,000)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Prioritäts-Support</span>
<span>Priority Support</span>
</li>
</>
)}
@ -494,7 +500,7 @@ export default function DashboardPage() {
}}
className="w-full"
>
Ersten QR-Code erstellen
Create First QR Code
</Button>
</DialogFooter>
</DialogContent>

View File

@ -4,12 +4,14 @@ 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 { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
import ChangePasswordModal from '@/components/settings/ChangePasswordModal';
type TabType = 'profile' | 'subscription';
export default function SettingsPage() {
const { fetchWithCsrf } = useCsrf();
const [activeTab, setActiveTab] = useState<TabType>('profile');
const [loading, setLoading] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
@ -64,9 +66,8 @@ export default function SettingsPage() {
try {
// Save to backend API
const response = await fetch('/api/user/profile', {
const response = await fetchWithCsrf('/api/user/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
@ -97,9 +98,8 @@ export default function SettingsPage() {
setLoading(true);
try {
const response = await fetch('/api/stripe/portal', {
const response = await fetchWithCsrf('/api/stripe/portal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
@ -134,9 +134,8 @@ export default function SettingsPage() {
setLoading(true);
try {
const response = await fetch('/api/user/delete', {
const response = await fetchWithCsrf('/api/user/delete', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();

View File

@ -7,10 +7,12 @@ 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('');
@ -36,9 +38,8 @@ export default function SignupPage() {
}
try {
const response = await fetch('/api/auth/signup', {
const response = await fetchWithCsrf('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password }),
});

File diff suppressed because it is too large Load Diff

View File

@ -45,32 +45,41 @@ export async function generateMetadata(): Promise<Metadata> {
}
const blogPosts = [
{
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: '8 Min',
readTime: '15 Min',
category: 'Analytics',
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=80',
},
{
slug: 'qr-codes-im-restaurant',
title: 'QR Codes in Restaurants: The Digital Menu Revolution',
excerpt: 'Discover how QR codes are revolutionizing the restaurant industry and what benefits they offer for restaurants and guests.',
date: 'January 15, 2025',
readTime: '5 Min',
category: 'Restaurant',
image: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&q=80',
},
{
slug: 'dynamische-vs-statische-qr-codes',
title: 'Dynamic vs Static QR Codes: What\'s the Difference?',
excerpt: 'A comprehensive comparison between dynamic and static QR codes and when you should use each type.',
date: 'January 10, 2025',
readTime: '3 Min',
category: 'Basics',
image: 'https://images.unsplash.com/photo-1603791440384-56cd371ee9a7?w=800&q=80',
image: '/blog/4-hero.png',
},
];

View File

@ -0,0 +1,387 @@
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';
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. Perfect for products, events, inventory management. Free bulk QR code generator with custom branding.',
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk qr codes free',
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.',
type: 'website',
},
};
export default function BulkQRCodeGeneratorPage() {
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 },
];
return (
<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">
<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. Perfect for products, events, inventory, and marketing campaigns. Fast, efficient, and with your branding.
</p>
<div className="space-y-3">
{[
'Upload Excel or CSV files',
'Generate up to 1000 QR codes',
'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>
{/* 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>
);
}

View File

@ -0,0 +1,393 @@
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';
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',
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.',
type: 'website',
},
};
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.',
},
];
return (
<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">
<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>
);
}

View File

@ -0,0 +1,306 @@
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';
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',
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.',
type: 'website',
},
};
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 },
];
return (
<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">
<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>
);
}

View File

@ -1,12 +1,34 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
try {
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.ANALYTICS);
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' }, { status: 401 });
}

View File

@ -6,12 +6,7 @@ import { z } from 'zod';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
const signupSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
});
import { signupSchema, validateRequest } from '@/lib/validationSchemas';
export async function POST(request: NextRequest) {
try {
@ -46,7 +41,14 @@ export async function POST(request: NextRequest) {
}
const body = await request.json();
const { name, email, password } = signupSchema.parse(body);
// Validate request body
const validation = await validateRequest(signupSchema, body);
if (!validation.success) {
return NextResponse.json(validation.error, { status: 400 });
}
const { name, email, password } = validation.data;
// Check if user already exists
const existingUser = await db.user.findUnique({

View File

@ -5,6 +5,7 @@ import { cookies } from 'next/headers';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
import { loginSchema, validateRequest } from '@/lib/validationSchemas';
export async function POST(request: NextRequest) {
try {
@ -38,7 +39,15 @@ export async function POST(request: NextRequest) {
);
}
const { email, password } = await request.json();
const body = await request.json();
// Validate request body
const validation = await validateRequest(loginSchema, body);
if (!validation.success) {
return NextResponse.json(validation.error, { status: 400 });
}
const { email, password } = validation.data;
// Find user
const user = await db.user.findUnique({

View File

@ -4,6 +4,7 @@ import { authOptions } from '@/lib/auth';
import { db } from '@/lib/db';
import { generateSlug } from '@/lib/hash';
import { z } from 'zod';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
const bulkCreateSchema = z.object({
qrCodes: z.array(z.object({
@ -22,6 +23,27 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Rate Limiting (user-based)
const clientId = session.user.id || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.BULK_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(),
}
}
);
}
const body = await request.json();
const { qrCodes } = bulkCreateSchema.parse(body);

View File

@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { z } from 'zod';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
const updateQRSchema = z.object({
title: z.string().min(1).optional(),
@ -55,7 +57,34 @@ export async function PATCH(
{ params }: { params: { id: string } }
) {
try {
// CSRF Protection
const csrfCheck = csrfProtection(request);
if (!csrfCheck.valid) {
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
}
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.QR_MODIFY);
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' }, { status: 401 });
}
@ -118,7 +147,34 @@ export async function DELETE(
{ params }: { params: { id: string } }
) {
try {
// CSRF Protection
const csrfCheck = csrfProtection(request);
if (!csrfCheck.valid) {
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
}
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.QR_MODIFY);
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' }, { status: 401 });
}

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function DELETE(request: NextRequest) {
try {
@ -15,6 +16,27 @@ export async function DELETE(request: NextRequest) {
}
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.QR_DELETE_ALL);
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' }, { status: 401 });
}

View File

@ -2,6 +2,9 @@ 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) {
@ -48,9 +51,36 @@ const PLAN_LIMITS = {
// 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 });
}
@ -70,6 +100,15 @@ export async function POST(request: NextRequest) {
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;

View File

@ -2,10 +2,32 @@ import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function POST(request: NextRequest) {
try {
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CANCEL);
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' }, { status: 401 });
}

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { stripe, STRIPE_PLANS } from '@/lib/stripe';
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function POST(request: NextRequest) {
try {
@ -9,6 +10,27 @@ export async function POST(request: NextRequest) {
const cookieStore = await cookies();
const userId = cookieStore.get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_CHECKOUT);
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 - Please log in' }, { status: 401 });
}

View File

@ -2,10 +2,32 @@ import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function POST(request: NextRequest) {
try {
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.STRIPE_PORTAL);
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' }, { status: 401 });
}

View File

@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { stripe } from '@/lib/stripe';
import { csrfProtection } from '@/lib/csrf';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function DELETE(request: NextRequest) {
try {
@ -16,6 +17,27 @@ export async function DELETE(request: NextRequest) {
}
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.ACCOUNT_DELETE);
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' }, { status: 401 });
}

View File

@ -3,6 +3,8 @@ import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';
import { csrfProtection } from '@/lib/csrf';
import { changePasswordSchema, validateRequest } from '@/lib/validationSchemas';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function PATCH(request: NextRequest) {
try {
@ -16,26 +18,40 @@ export async function PATCH(request: NextRequest) {
}
const userId = cookies().get('userId')?.value;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.PASSWORD_CHANGE);
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' }, { status: 401 });
}
const body = await request.json();
const { currentPassword, newPassword } = body;
if (!currentPassword || !newPassword) {
return NextResponse.json(
{ error: 'Current password and new password are required' },
{ status: 400 }
);
// Validate request body
const validation = await validateRequest(changePasswordSchema, body);
if (!validation.success) {
return NextResponse.json(validation.error, { status: 400 });
}
if (newPassword.length < 8) {
return NextResponse.json(
{ error: 'New password must be at least 8 characters' },
{ status: 400 }
);
}
const { currentPassword, newPassword } = validation.data;
// Get user with password
const user = await db.user.findUnique({
@ -50,6 +66,14 @@ export async function PATCH(request: NextRequest) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Check if user has a password (OAuth users don't have passwords)
if (!user.password) {
return NextResponse.json(
{ error: 'Cannot change password for OAuth accounts' },
{ status: 400 }
);
}
// Verify current password
const isPasswordValid = await bcrypt.compare(currentPassword, user.password);
if (!isPasswordValid) {

View File

@ -1,24 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { csrfProtection } from '@/lib/csrf';
import { updateProfileSchema, validateRequest } from '@/lib/validationSchemas';
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export async function PATCH(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;
// Rate Limiting (user-based)
const clientId = userId || getClientIdentifier(request);
const rateLimitResult = rateLimit(clientId, RateLimits.PROFILE_UPDATE);
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' }, { status: 401 });
}
const body = await request.json();
const { name } = body;
if (!name || name.trim().length === 0) {
return NextResponse.json(
{ error: 'Name is required' },
{ status: 400 }
);
// Validate request body
const validation = await validateRequest(updateProfileSchema, body);
if (!validation.success) {
return NextResponse.json(validation.error, { status: 400 });
}
const { name } = validation.data;
// Update user name in database
const updatedUser = await db.user.update({
where: { id: userId },

119
src/app/error.tsx Normal file
View File

@ -0,0 +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>
);
}

83
src/app/not-found.tsx Normal file
View File

@ -0,0 +1,83 @@
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 Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-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>
Go Home
</Button>
</Link>
<Link href="/create">
<Button variant="outline" size="lg">
Create QR Code
</Button>
</Link>
</div>
{/* Help Text */}
<div className="mt-12 pt-8 border-t border-gray-200">
<p className="text-sm text-gray-500">
Need help?{' '}
<Link href="/#faq" className="text-primary-600 hover:text-primary-700 font-medium">
Visit our FAQ
</Link>{' '}
or{' '}
<Link href="/blog" className="text-primary-600 hover:text-primary-700 font-medium">
read our blog
</Link>
</p>
</div>
</div>
</div>
);
}

21
src/app/robots.ts Normal file
View File

@ -0,0 +1,21 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://www.qrmaster.com';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/dashboard/',
'/create/',
'/settings/',
],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}

74
src/app/sitemap.ts Normal file
View File

@ -0,0 +1,74 @@
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://www.qrmaster.com';
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/qr-code-tracking`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/dynamic-qr-code-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/bulk-qr-code-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/pricing`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/faq`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/signup`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/login`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.5,
},
{
url: `${baseUrl}/privacy`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.4,
},
{
url: `${baseUrl}/terms`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.4,
},
];
}

View File

@ -67,7 +67,7 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
<CardContent className="space-y-4">
<ul className="space-y-3">
{t.pricing[plan.key].features.slice(0, 3).map((feature: string, index: number) => (
{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" />

View File

@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { showToast } from '@/components/ui/Toast';
import { useCsrf } from '@/hooks/useCsrf';
interface ChangePasswordModalProps {
isOpen: boolean;
@ -15,6 +16,7 @@ export default function ChangePasswordModal({
onClose,
onSuccess,
}: ChangePasswordModalProps) {
const { fetchWithCsrf } = useCsrf();
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
@ -47,9 +49,8 @@ export default function ChangePasswordModal({
setLoading(true);
try {
const response = await fetch('/api/user/password', {
const response = await fetchWithCsrf('/api/user/password', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentPassword,
newPassword,

View File

@ -29,9 +29,9 @@ export function useCsrf() {
* Helper function to add CSRF token to fetch headers
*/
const getHeaders = (additionalHeaders: HeadersInit = {}) => {
const headers: HeadersInit = {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...additionalHeaders,
...(additionalHeaders as Record<string, string>),
};
if (csrfToken) {

View File

@ -19,14 +19,15 @@ export function getAuthCookieOptions() {
/**
* Get cookie options for CSRF tokens
* Note: httpOnly is false so client-side JavaScript can read the token
* Note: httpOnly is false so the client can read it, but we verify via double-submit pattern
*/
export function getCsrfCookieOptions() {
return {
httpOnly: false, // Client needs to read this token
httpOnly: false, // Client needs to read this token for the header
secure: isProduction, // HTTPS only in production
sameSite: 'lax' as const,
maxAge: 60 * 60 * 24, // 24 hours
path: '/', // Available on all paths
};
}

View File

@ -14,7 +14,8 @@ const rateLimitStore = new Map<string, RateLimitEntry>();
// Cleanup old entries every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [key, entry] of rateLimitStore.entries()) {
const entries = Array.from(rateLimitStore.entries());
for (const [key, entry] of entries) {
if (entry.resetAt < now) {
rateLimitStore.delete(key);
}
@ -108,6 +109,7 @@ export function getClientIdentifier(request: Request): string {
* Predefined rate limit configurations
*/
export const RateLimits = {
// Auth endpoints
// Login: 5 attempts per 15 minutes
LOGIN: {
name: 'login',
@ -122,17 +124,98 @@ export const RateLimits = {
windowSeconds: 60 * 60,
},
// API: 100 requests per minute
API: {
name: 'api',
maxRequests: 100,
windowSeconds: 60,
},
// Password reset: 3 attempts per hour
PASSWORD_RESET: {
name: 'password-reset',
maxRequests: 3,
windowSeconds: 60 * 60,
},
// QR Code endpoints
// Create QR: 20 per minute
QR_CREATE: {
name: 'qr-create',
maxRequests: 20,
windowSeconds: 60,
},
// Modify QR: 30 per minute
QR_MODIFY: {
name: 'qr-modify',
maxRequests: 30,
windowSeconds: 60,
},
// Delete all QRs: 3 per hour
QR_DELETE_ALL: {
name: 'qr-delete-all',
maxRequests: 3,
windowSeconds: 60 * 60,
},
// Bulk create: 3 per hour
BULK_CREATE: {
name: 'bulk-create',
maxRequests: 3,
windowSeconds: 60 * 60,
},
// User settings endpoints
// Profile update: 10 per minute
PROFILE_UPDATE: {
name: 'profile-update',
maxRequests: 10,
windowSeconds: 60,
},
// Password change: 5 per hour
PASSWORD_CHANGE: {
name: 'password-change',
maxRequests: 5,
windowSeconds: 60 * 60,
},
// Account delete: 2 per day
ACCOUNT_DELETE: {
name: 'account-delete',
maxRequests: 2,
windowSeconds: 24 * 60 * 60,
},
// Analytics endpoints
// Analytics summary: 30 per minute
ANALYTICS: {
name: 'analytics',
maxRequests: 30,
windowSeconds: 60,
},
// Stripe endpoints
// Checkout session: 5 per minute
STRIPE_CHECKOUT: {
name: 'stripe-checkout',
maxRequests: 5,
windowSeconds: 60,
},
// Customer portal: 10 per minute
STRIPE_PORTAL: {
name: 'stripe-portal',
maxRequests: 10,
windowSeconds: 60,
},
// Cancel subscription: 3 per hour
STRIPE_CANCEL: {
name: 'stripe-cancel',
maxRequests: 3,
windowSeconds: 60 * 60,
},
// General API: 100 requests per minute
API: {
name: 'api',
maxRequests: 100,
windowSeconds: 60,
},
};

View File

@ -0,0 +1,174 @@
/**
* 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', 'WIFI', 'EMAIL', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT'], {
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', 'WIFI', 'EMAIL', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT']),
})
).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'),
});
// ==========================================
// 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' } };
}
}