Wichige änderung an DB

This commit is contained in:
Timo Knuth 2025-11-05 12:02:59 +01:00
parent 2f0208ebf9
commit f31992b952
37 changed files with 2774 additions and 2596 deletions

View File

@ -313,3 +313,9 @@ For issues:
🎉 **Congratulations!** You've successfully migrated from Supabase to local PostgreSQL!

242
README.md
View File

@ -1,28 +1,31 @@
# QR Master - Create Custom QR Codes in Seconds
A production-ready SaaS application for creating and managing QR codes with advanced tracking, analytics, and integrations.
A production-ready SaaS application for creating and managing QR codes with advanced tracking, analytics, and Stripe payment integration.
## Features
- 🎨 **Custom QR Codes** - Create static and dynamic QR codes with full customization
- 📊 **Advanced Analytics** - Track scans, locations, devices, and user behavior
- 🔄 **Dynamic Content** - Edit QR code destinations anytime without reprinting
- 📦 **Bulk Operations** - Import CSV/Excel files to create multiple QR codes at once
- 🔌 **Integrations** - Connect with Zapier, Airtable, and Google Sheets
- 🌍 **Multi-language** - Support for English and German (i18n)
- 🔒 **Privacy-First** - Respects user privacy with hashed IPs and DNT headers
- 📦 **Bulk Operations** - Import CSV/Excel files to create up to 1,000 QR codes at once
- 💳 **Stripe Integration** - FREE, PRO, and BUSINESS subscription plans with secure billing
- 🎨 **Custom Branding** - Logo upload, custom colors (PRO+ plans)
- 🌍 **SEO Optimized** - Schema.org structured data, meta tags, breadcrumbs
- 🔒 **Privacy-First** - GDPR-compliant, hashed IPs, DNT headers respected
- 📱 **Responsive Design** - Works perfectly on all devices
## Tech Stack
- **Frontend**: Next.js 14 (App Router), TypeScript, Tailwind CSS
- **Backend**: Next.js API Routes, Prisma ORM
- **Database**: PostgreSQL
- **Database**: PostgreSQL (with Prisma migrations)
- **Cache**: Redis (optional)
- **Auth**: NextAuth.js (Credentials + Google OAuth)
- **Payments**: Stripe (Subscriptions & Webhooks)
- **QR Generation**: qrcode library
- **Charts**: Chart.js with react-chartjs-2
- **i18n**: i18next
- **Bulk Processing**: Papa Parse (CSV), XLSX, JSZip
- **Analytics**: PostHog (optional)
- **SEO**: next-sitemap, Schema.org structured data
## Quick Start
@ -63,12 +66,18 @@ Edit `.env` and set:
npm run docker:dev
```
5. Run migrations and seed:
5. Run database migrations and seed:
```bash
npm run db:migrate
npx prisma migrate dev
npm run db:seed
```
> **Note**: If you get migration errors, you can reset the database:
> ```bash
> npx prisma migrate reset
> ```
> This will drop the database, recreate it, run all migrations, and seed data.
6. Start development server:
```bash
npm run dev
@ -76,8 +85,8 @@ npm run dev
7. Access the application:
- **App**: http://localhost:3050
- **Database UI**: http://localhost:8080 (Adminer)
- **Database**: localhost:5432
- **Database UI**: http://localhost:8080 (Adminer - username: `root`, password: `root`)
- **Database**: localhost:5435 (username: `postgres`, password: `postgres`)
- **Redis**: localhost:6379
#### Option 2: Full Docker (Production)
@ -108,10 +117,13 @@ docker-compose exec web npx prisma migrate deploy
## Demo Account
Use these credentials to test the application:
After running `npm run db:seed`, use these credentials to test the application:
- **Email**: demo@qrmaster.com
- **Password**: demo123
- **Plan**: FREE (3 QR codes limit)
The seed script also creates sample QR codes for testing.
## Development
@ -119,25 +131,28 @@ Use these credentials to test the application:
```bash
# Development
npm run dev # Start Next.js dev server
npm run build # Build for production
npm run start # Start production server
npm run dev # Start Next.js dev server (port 3050)
npm run build # Build for production
npm run start # Start production server
# Database
npm run db:generate # Generate Prisma Client
npm run db:migrate # Run migrations (dev)
npm run db:deploy # Deploy migrations (prod)
npm run db:seed # Seed database
npm run db:studio # Open Prisma Studio
npm run db:generate # Generate Prisma Client
npm run db:migrate # Run migrations (dev mode)
npm run db:deploy # Deploy migrations (production)
npm run db:seed # Seed database with demo data
npm run db:studio # Open Prisma Studio UI
npx prisma migrate reset # Reset database (drop, recreate, migrate, seed)
# Docker
npm run docker:dev # Start DB & Redis only
npm run docker:dev:stop # Stop dev services
npm run docker:prod # Start full stack
npm run docker:stop # Stop all services
npm run docker:logs # View logs
npm run docker:db # PostgreSQL CLI
npm run docker:redis # Redis CLI
npm run docker:dev # Start DB & Redis only
npm run docker:dev:stop # Stop dev services
npm run docker:dev:clean # Stop and clean containers
npm run docker:prod # Start full stack (production)
npm run docker:stop # Stop all services
npm run docker:logs # View container logs
npm run docker:db # PostgreSQL CLI
npm run docker:redis # Redis CLI
npm run docker:backup # Backup database to SQL file
```
### Local Development (without Docker)
@ -151,12 +166,12 @@ npm install
3. Configure `.env` with local database URL:
```env
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/qrmaster?schema=public
DATABASE_URL=postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public
```
4. Run migrations:
4. Run migrations and seed:
```bash
npm run db:migrate
npx prisma migrate dev
npm run db:seed
```
@ -165,6 +180,24 @@ npm run db:seed
npm run dev
```
### Resetting the Database
If you need to reset your database (drop all tables, recreate, and reseed):
```bash
# Full reset (drops database, reruns migrations, seeds data)
npx prisma migrate reset
# Or manually:
npx prisma migrate reset --skip-seed # Reset without seeding
npm run db:seed # Then seed manually
```
This is useful when:
- Schema has changed significantly
- You have migration conflicts
- You want to start fresh with clean data
### Project Structure
```
@ -197,16 +230,28 @@ qr-master/
### QR Codes
- `GET /api/qrs` - List all QR codes
- `POST /api/qrs` - Create a new QR code
- `POST /api/qrs` - Create a new QR code (dynamic or static)
- `POST /api/qrs/static` - Create a static QR code
- `GET /api/qrs/[id]` - Get QR code details
- `PATCH /api/qrs/[id]` - Update QR code
- `DELETE /api/qrs/[id]` - Delete QR code
- `DELETE /api/qrs/delete-all` - Delete all user's QR codes
### Analytics
- `GET /api/analytics/summary` - Get analytics summary
- `GET /api/analytics/summary` - Get analytics summary for a QR code
### Bulk Operations
- `POST /api/bulk` - Import QR codes from CSV/Excel
### User & Settings
- `GET /api/user/plan` - Get current user plan
- `GET /api/user/stats` - Get user statistics
- `POST /api/user/password` - Update password
- `POST /api/user/profile` - Update profile
- `DELETE /api/user/delete` - Delete account
### Stripe Payments
- `POST /api/stripe/checkout` - Create checkout session
- `POST /api/stripe/portal` - Create customer portal session
- `POST /api/stripe/webhook` - Handle Stripe webhooks
- `POST /api/stripe/cancel-subscription` - Cancel subscription
### Public Redirect
- `GET /r/[slug]` - Redirect and track QR code scan
@ -215,24 +260,67 @@ qr-master/
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `DATABASE_URL` | PostgreSQL connection string | Yes | `postgresql://postgres:postgres@localhost:5432/qrmaster?schema=public` |
| `DATABASE_URL` | PostgreSQL connection string | Yes | `postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public` |
| `NEXTAUTH_URL` | Application URL | Yes | `http://localhost:3050` |
| `NEXTAUTH_SECRET` | Secret for JWT encryption | Yes | Generate with `openssl rand -base64 32` |
| `IP_SALT` | Salt for IP hashing | Yes | Generate with `openssl rand -base64 32` |
| `IP_SALT` | Salt for IP hashing (privacy) | Yes | Generate with `openssl rand -base64 32` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | No | - |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | No | - |
| `STRIPE_SECRET_KEY` | Stripe secret key | No | - |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | No | - |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe public key | No | - |
| `NEXT_PUBLIC_INDEXABLE` | Allow search engine indexing | No | `false` (set to `true` in production) |
| `REDIS_URL` | Redis connection string | No | `redis://redis:6379` |
| `ENABLE_DEMO` | Enable demo mode | No | `false` |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog analytics key | No | - |
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host URL | No | - |
**Note**: Copy `env.example` to `.env` and update the values before starting.
## Security
### Generating Secrets
- IP addresses are hashed with salt before storage
- Respects Do Not Track (DNT) headers
- CORS protection enabled
- Rate limiting on API endpoints
- Secure session management with NextAuth.js
```bash
# Generate NEXTAUTH_SECRET
openssl rand -base64 32
# Generate IP_SALT
openssl rand -base64 32
```
## Security & Privacy
- **IP Hashing**: IP addresses are hashed with salt before storage (GDPR-compliant)
- **DNT Respect**: Honors Do Not Track browser headers
- **Rate Limiting**: API endpoints protected against abuse
- **CSRF Protection**: Token-based CSRF validation on mutations
- **Secure Sessions**: NextAuth.js with encrypted JWT tokens
- **Stripe Security**: PCI-compliant payment processing
- **SQL Injection Prevention**: Prisma ORM parameterized queries
## Database Schema
The application uses PostgreSQL with Prisma ORM. Key models:
- **User**: User accounts with Stripe subscription data
- **QRCode**: QR code records (static/dynamic, multiple content types)
- **QRScan**: Scan analytics data (hashed IP, device, location, UTM params)
- **Integration**: Third-party integrations (Zapier, etc.)
- **Account/Session**: NextAuth authentication data
### Supported QR Code Types
- **URL**: Website links
- **VCARD**: Contact cards (name, email, phone, company)
- **GEO**: GPS locations
- **PHONE**: Phone numbers (tel: links)
- **TEXT**: Plain text
- **SMS**: SMS messages
- **WHATSAPP**: WhatsApp messages
### Plans
- **FREE**: 3 dynamic QR codes, unlimited static
- **PRO**: 50 codes, custom branding, advanced analytics
- **BUSINESS**: 500 codes, bulk upload, API access, priority support
## Deployment
@ -263,6 +351,72 @@ For detailed deployment instructions, see [DOCKER_SETUP.md](DOCKER_SETUP.md).
**Note**: For Vercel deployment, you'll need to set up a PostgreSQL database separately.
## Troubleshooting
### Database Issues
**Problem**: Migration errors or schema conflicts
```bash
# Solution: Reset the database
npx prisma migrate reset
```
**Problem**: "Error: P1001: Can't reach database server"
```bash
# Check if Docker containers are running
docker ps
# Restart database
npm run docker:dev:stop
npm run docker:dev
```
**Problem**: Prisma Client out of sync
```bash
# Regenerate Prisma Client
npx prisma generate
```
**Problem**: Need to start completely fresh
```bash
# Stop all Docker containers
npm run docker:dev:stop
# Remove volumes (⚠️ deletes all data)
docker volume prune
# Restart everything
npm run docker:dev
npx prisma migrate dev
npm run db:seed
```
### Port Already in Use
If port 3050 is already in use:
```bash
# Find and kill the process (Windows)
netstat -ano | findstr :3050
taskkill /PID <PID> /F
# Or change the port in package.json
"dev": "next dev -p 3051"
```
### Docker Issues
**Problem**: Permission denied errors
```bash
# Windows: Run PowerShell as Administrator
# Linux/Mac: Use sudo for docker commands
```
**Problem**: Out of disk space
```bash
# Clean up Docker
docker system prune -a
```
## Contributing
1. Fork the repository

View File

@ -4,8 +4,8 @@ PORT=3000
# Database Configuration (PostgreSQL)
# For local development (without Docker):
# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/qrmaster?schema=public
# For Docker Compose:
# DATABASE_URL=postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public
# For Docker Compose (internal Docker network):
DATABASE_URL=postgresql://postgres:postgres@db:5432/qrmaster?schema=public
# NextAuth Configuration
@ -26,3 +26,21 @@ IP_SALT=your-ip-salt-here-change-in-production
# Features
ENABLE_DEMO=false
# SEO Configuration
# Set to 'true' in production to allow search engine indexing
NEXT_PUBLIC_INDEXABLE=true
# Stripe Payment Configuration (Optional - for subscription payments)
# Get your keys from: https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# Stripe Price IDs (create these in your Stripe dashboard)
# NEXT_PUBLIC_STRIPE_FREE_PRICE_ID=price_xxx
# NEXT_PUBLIC_STRIPE_PRO_PRICE_ID=price_xxx
# NEXT_PUBLIC_STRIPE_BUSINESS_PRICE_ID=price_xxx
# Analytics (Optional - PostHog)
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com

View File

@ -2,8 +2,11 @@
const nextConfig = {
output: 'standalone',
images: {
unoptimized: true,
domains: ['www.qrmaster.com', 'qrmaster.com', 'images.qrmaster.com']
unoptimized: false,
domains: ['www.qrmaster.com', 'qrmaster.com', 'images.qrmaster.com'],
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
experimental: {
serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs']

1474
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@
"next-auth": "^4.24.5",
"papaparse": "^5.4.1",
"posthog-js": "^1.276.0",
"qr-code-styling": "^1.9.2",
"qrcode": "^1.5.3",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",

View File

@ -2,7 +2,8 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
@ -101,10 +102,9 @@ enum QRType {
enum ContentType {
URL
WIFI
VCARD
GEO
PHONE
EMAIL
SMS
TEXT
WHATSAPP

View File

@ -50,11 +50,18 @@ async function main() {
slug: 'company-website-qr',
},
{
title: 'Contact Email',
contentType: 'EMAIL' as const,
content: { email: 'contact@company.com', subject: 'Inquiry' },
tags: ['contact', 'email'],
slug: 'contact-email-qr',
title: 'Contact Card',
contentType: 'VCARD' as const,
content: {
firstName: 'John',
lastName: 'Doe',
email: 'john@company.com',
phone: '+1234567890',
organization: 'Example Corp',
title: 'CEO'
},
tags: ['contact', 'vcard'],
slug: 'contact-card-qr',
},
{
title: 'Event Details',

View File

@ -331,19 +331,19 @@ export default function AnalyticsPage() {
<Table>
<thead>
<tr>
<th>Country</th>
<th>Scans</th>
<th>Percentage</th>
<th>Trend</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Country</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Scans</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Percentage</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Trend</th>
</tr>
</thead>
<tbody>
{analyticsData.countryStats.map((country: any, index: number) => (
<tr key={index}>
<td>{country.country}</td>
<td>{country.count.toLocaleString()}</td>
<td>{country.percentage}%</td>
<td>
<tr key={index} className="border-b transition-colors hover:bg-gray-50/50">
<td className="px-4 py-4 align-middle">{country.country}</td>
<td className="px-4 py-4 align-middle">{country.count.toLocaleString()}</td>
<td className="px-4 py-4 align-middle">{country.percentage}%</td>
<td className="px-4 py-4 align-middle">
<Badge variant="success"></Badge>
</td>
</tr>
@ -366,27 +366,27 @@ export default function AnalyticsPage() {
<Table>
<thead>
<tr>
<th>QR Code</th>
<th>Type</th>
<th>Total Scans</th>
<th>Unique Scans</th>
<th>Conversion</th>
<th>Trend</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">QR Code</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Type</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Total Scans</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Unique Scans</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Conversion</th>
<th className="h-12 px-4 text-left align-middle font-medium text-gray-500">Trend</th>
</tr>
</thead>
<tbody>
{analyticsData.qrPerformance.map((qr: any) => (
<tr key={qr.id}>
<td className="font-medium">{qr.title}</td>
<td>
<tr key={qr.id} className="border-b transition-colors hover:bg-gray-50/50">
<td className="px-4 py-4 align-middle font-medium">{qr.title}</td>
<td className="px-4 py-4 align-middle">
<Badge variant={qr.type === 'DYNAMIC' ? 'info' : 'default'}>
{qr.type}
</Badge>
</td>
<td>{qr.totalScans.toLocaleString()}</td>
<td>{qr.uniqueScans.toLocaleString()}</td>
<td>{qr.conversion}%</td>
<td>
<td className="px-4 py-4 align-middle">{qr.totalScans.toLocaleString()}</td>
<td className="px-4 py-4 align-middle">{qr.uniqueScans.toLocaleString()}</td>
<td className="px-4 py-4 align-middle">{qr.conversion}%</td>
<td className="px-4 py-4 align-middle">
<Badge variant={qr.totalScans > 0 ? 'success' : 'default'}>
{qr.totalScans > 0 ? '↑' : '—'}
</Badge>

View File

@ -233,6 +233,14 @@ export default function BulkCreationPage() {
{ title: 'Product Page', content: 'https://example.com/product' },
{ title: 'Landing Page', content: 'https://example.com/landing' },
{ title: 'Contact Form', content: 'https://example.com/contact' },
{ title: 'About Us', content: 'https://example.com/about' },
{ title: 'Pricing Page', content: 'https://example.com/pricing' },
{ title: 'FAQ Page', content: 'https://example.com/faq' },
{ title: 'Blog Article', content: 'https://example.com/blog/article-1' },
{ title: 'Support Portal', content: 'https://example.com/support' },
{ title: 'Download Page', content: 'https://example.com/download' },
{ title: 'Social Media', content: 'https://instagram.com/yourcompany' },
{ title: 'YouTube Video', content: 'https://youtube.com/watch?v=example' },
];
const csv = Papa.unparse(template);
@ -321,41 +329,35 @@ export default function BulkCreationPage() {
<div className="mb-8">
<div className="flex items-center justify-between">
<div className={`flex items-center ${step === 'upload' ? 'text-primary-600' : 'text-gray-400'}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
step === 'upload' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step === 'upload' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
1
</div>
<span className="ml-3 font-medium">Upload File</span>
</div>
<div className="flex-1 h-0.5 bg-gray-200 mx-4">
<div className={`h-full bg-primary-600 transition-all ${
step === 'preview' || step === 'complete' ? 'w-full' : 'w-0'
}`} />
<div className={`h-full bg-primary-600 transition-all ${step === 'preview' || step === 'complete' ? 'w-full' : 'w-0'
}`} />
</div>
<div className={`flex items-center ${
step === 'preview' || step === 'complete' ? 'text-primary-600' : 'text-gray-400'
}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
step === 'preview' || step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
<div className={`flex items-center ${step === 'preview' || step === 'complete' ? 'text-primary-600' : 'text-gray-400'
}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step === 'preview' || step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
2
</div>
<span className="ml-3 font-medium">Preview & Map</span>
</div>
<div className="flex-1 h-0.5 bg-gray-200 mx-4">
<div className={`h-full bg-primary-600 transition-all ${
step === 'complete' ? 'w-full' : 'w-0'
}`} />
<div className={`h-full bg-primary-600 transition-all ${step === 'complete' ? 'w-full' : 'w-0'
}`} />
</div>
<div className={`flex items-center ${step === 'complete' ? 'text-primary-600' : 'text-gray-400'}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
3
</div>
<span className="ml-3 font-medium">Download</span>
@ -378,9 +380,8 @@ export default function BulkCreationPage() {
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${
isDragActive ? 'border-primary-500 bg-primary-50' : 'border-gray-300 hover:border-gray-400'
}`}
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${isDragActive ? 'border-primary-500 bg-primary-50' : 'border-gray-300 hover:border-gray-400'
}`}
>
<input {...getInputProps()} />
<svg className="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -442,6 +443,110 @@ export default function BulkCreationPage() {
</CardContent>
</Card>
</div>
{/* Supported QR Code Types Section */}
<div className="mt-8">
<Card>
<CardHeader>
<CardTitle className="text-lg">📋 Supported QR Code Types</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<p className="text-gray-600 mb-6">
This bulk generator creates <strong>static QR codes</strong> for multiple content types. Choose the format that matches your needs:
</p>
<div className="space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">🌐 URL - Website Links</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">https://example.com</code></p>
<p className="text-xs text-gray-500">Example: Product Page,https://example.com/product</p>
</div>
<div className="border-l-4 border-purple-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">👤 VCARD - Contact Cards</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">FirstName,LastName,Email,Phone,Organization,Title</code></p>
<p className="text-xs text-gray-500">Example: John Doe,"John,Doe,john@example.com,+1234567890,Company,CEO"</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">📍 GEO - Locations</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">latitude,longitude,label</code></p>
<p className="text-xs text-gray-500">Example: Office Location,"37.7749,-122.4194,Main Office"</p>
</div>
<div className="border-l-4 border-pink-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">📞 PHONE - Phone Numbers</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">+1234567890</code></p>
<p className="text-xs text-gray-500">Example: Support Hotline,+1234567890</p>
</div>
<div className="border-l-4 border-yellow-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">📝 TEXT - Plain Text</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">Any text content</code></p>
<p className="text-xs text-gray-500">Example: Serial Number,SN-12345-ABCDE</p>
</div>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-6 mt-6">
<h4 className="font-semibold text-gray-900 mb-3">📥 CSV File Format:</h4>
<p className="text-sm text-gray-600 mb-3">
Your file needs <strong>two columns</strong>: <code className="bg-white px-2 py-1 rounded">title</code> and <code className="bg-white px-2 py-1 rounded">content</code>
</p>
<div className="bg-white rounded-lg p-4 shadow-sm overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b-2 border-gray-300">
<th className="text-left py-2 px-3 font-semibold text-gray-700">title</th>
<th className="text-left py-2 px-3 font-semibold text-gray-700">content</th>
</tr>
</thead>
<tbody className="font-mono text-xs">
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Product Page</td>
<td className="py-2 px-3">https://example.com/product</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">John Doe</td>
<td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Office Location</td>
<td className="py-2 px-3">37.7749,-122.4194,Main Office</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Support Hotline</td>
<td className="py-2 px-3">+1234567890</td>
</tr>
<tr>
<td className="py-2 px-3">Serial Number</td>
<td className="py-2 px-3">SN-12345-ABCDE</td>
</tr>
</tbody>
</table>
</div>
</div>
<div className="border-l-4 border-yellow-500 pl-4">
<p className="font-semibold text-gray-900 mb-1"> Important Notes</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> <strong>Static QR codes</strong> - Cannot be edited after creation</li>
<li> <strong>No tracking or analytics</strong> - Scans are not tracked</li>
<li> <strong>Maximum 1,000 QR codes</strong> per upload</li>
<li> <strong>Download as ZIP</strong> or save to your dashboard</li>
<li> <strong>All QR types supported</strong> - URLs, vCards, locations, phone numbers, and text</li>
</ul>
</div>
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-900">
<strong>💡 Tip:</strong> Download the template above to see examples of all 5 QR code types with 11 ready-to-use examples!
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
)}

View File

@ -1,410 +0,0 @@
'use client';
import React, { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import Papa from 'papaparse';
import * as XLSX from 'xlsx';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Select } from '@/components/ui/Select';
import { useTranslation } from '@/hooks/useTranslation';
import { QRCodeSVG } from 'qrcode.react';
interface BulkQRData {
title: string;
contentType: string;
content: string;
tags?: string;
type?: 'STATIC' | 'DYNAMIC';
}
export default function BulkUploadPage() {
const { t } = useTranslation();
const [step, setStep] = useState<'upload' | 'preview' | 'complete'>('upload');
const [data, setData] = useState<BulkQRData[]>([]);
const [mapping, setMapping] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const [uploadResult, setUploadResult] = useState<any>(null);
const onDrop = useCallback((acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) return;
const reader = new FileReader();
if (file.name.endsWith('.csv')) {
reader.onload = (e) => {
const text = e.target?.result as string;
const result = Papa.parse(text, { header: true });
processData(result.data);
};
reader.readAsText(file);
} else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
reader.onload = (e) => {
const data = new Uint8Array(e.target?.result as ArrayBuffer);
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
processData(jsonData);
};
reader.readAsArrayBuffer(file);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'text/csv': ['.csv'],
'application/vnd.ms-excel': ['.xls'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
},
maxFiles: 1,
});
const processData = (rawData: any[]) => {
// Auto-detect columns
if (rawData.length > 0) {
const columns = Object.keys(rawData[0]);
const autoMapping: Record<string, string> = {};
columns.forEach((col) => {
const lowerCol = col.toLowerCase();
if (lowerCol.includes('title') || lowerCol.includes('name')) {
autoMapping.title = col;
} else if (lowerCol.includes('type')) {
autoMapping.contentType = col;
} else if (lowerCol.includes('content') || lowerCol.includes('url') || lowerCol.includes('data')) {
autoMapping.content = col;
} else if (lowerCol.includes('tag')) {
autoMapping.tags = col;
}
});
setMapping(autoMapping);
}
setData(rawData);
setStep('preview');
};
const handleUpload = async () => {
setLoading(true);
try {
// Transform data based on mapping
const transformedData = data.map((row: any) => ({
title: row[mapping.title] || 'Untitled',
contentType: row[mapping.contentType] || 'URL',
content: row[mapping.content] || '',
tags: row[mapping.tags] || '',
type: 'DYNAMIC' as const,
}));
const response = await fetch('/api/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ qrCodes: transformedData }),
});
if (response.ok) {
const result = await response.json();
setUploadResult(result);
setStep('complete');
}
} catch (error) {
console.error('Bulk upload error:', error);
} finally {
setLoading(false);
}
};
const downloadTemplate = () => {
const template = [
{ title: 'Product Page', contentType: 'URL', content: 'https://example.com/product', tags: 'product,marketing' },
{ title: 'Contact Card', contentType: 'VCARD', content: 'John Doe', tags: 'contact,business' },
{ title: 'WiFi Network', contentType: 'WIFI', content: 'NetworkName:password123', tags: 'wifi,office' },
];
const csv = Papa.unparse(template);
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'qr-codes-template.csv';
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
</div>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div className={`flex items-center ${step === 'upload' ? 'text-primary-600' : 'text-gray-400'}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
step === 'upload' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
1
</div>
<span className="ml-3 font-medium">Upload File</span>
</div>
<div className="flex-1 h-0.5 bg-gray-200 mx-4">
<div className={`h-full bg-primary-600 transition-all ${
step === 'preview' || step === 'complete' ? 'w-full' : 'w-0'
}`} />
</div>
<div className={`flex items-center ${
step === 'preview' || step === 'complete' ? 'text-primary-600' : 'text-gray-400'
}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
step === 'preview' || step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
2
</div>
<span className="ml-3 font-medium">Preview & Map</span>
</div>
<div className="flex-1 h-0.5 bg-gray-200 mx-4">
<div className={`h-full bg-primary-600 transition-all ${
step === 'complete' ? 'w-full' : 'w-0'
}`} />
</div>
<div className={`flex items-center ${step === 'complete' ? 'text-primary-600' : 'text-gray-400'}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
3
</div>
<span className="ml-3 font-medium">Complete</span>
</div>
</div>
</div>
{/* Upload Step */}
{step === 'upload' && (
<Card>
<CardContent className="p-8">
<div className="text-center mb-6">
<Button variant="outline" onClick={downloadTemplate}>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download Template
</Button>
</div>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${
isDragActive ? 'border-primary-500 bg-primary-50' : 'border-gray-300 hover:border-gray-400'
}`}
>
<input {...getInputProps()} />
<svg className="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-lg font-medium text-gray-900 mb-2">
{isDragActive ? 'Drop the file here' : 'Drag & drop your file here'}
</p>
<p className="text-sm text-gray-500 mb-4">or click to browse</p>
<p className="text-xs text-gray-400">Supports CSV, XLS, XLSX (max 1000 rows)</p>
</div>
<div className="mt-8 grid md:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">CSV Format</p>
<p className="text-sm text-gray-500">Comma-separated values</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Excel Format</p>
<p className="text-sm text-gray-500">XLS or XLSX files</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Fast Processing</p>
<p className="text-sm text-gray-500">Up to 1000 QR codes</p>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
)}
{/* Preview Step */}
{step === 'preview' && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Preview & Map Columns</CardTitle>
<Badge variant="info">{data.length} rows detected</Badge>
</div>
</CardHeader>
<CardContent>
<div className="mb-6 grid md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title Column</label>
<Select
value={mapping.title || ''}
onChange={(e) => setMapping({ ...mapping, title: e.target.value })}
options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content Type Column</label>
<Select
value={mapping.contentType || ''}
onChange={(e) => setMapping({ ...mapping, contentType: e.target.value })}
options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content Column</label>
<Select
value={mapping.content || ''}
onChange={(e) => setMapping({ ...mapping, content: e.target.value })}
options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tags Column (Optional)</label>
<Select
value={mapping.tags || ''}
onChange={(e) => setMapping({ ...mapping, tags: e.target.value })}
options={[
{ value: '', label: 'None' },
...Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))
]}
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Preview</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Title</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Type</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Content</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Tags</th>
</tr>
</thead>
<tbody>
{data.slice(0, 5).map((row: any, index) => (
<tr key={index} className="border-b">
<td className="py-3 px-4">
<QRCodeSVG
value={row[mapping.content] || 'https://example.com'}
size={40}
/>
</td>
<td className="py-3 px-4 text-sm text-gray-900">
{row[mapping.title] || 'Untitled'}
</td>
<td className="py-3 px-4 text-sm text-gray-900">
{row[mapping.contentType] || 'URL'}
</td>
<td className="py-3 px-4 text-sm text-gray-900">
{(row[mapping.content] || '').substring(0, 30)}...
</td>
<td className="py-3 px-4 text-sm text-gray-900">
{row[mapping.tags] || '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{data.length > 5 && (
<p className="text-sm text-gray-500 mt-4 text-center">
Showing 5 of {data.length} rows
</p>
)}
<div className="flex justify-between mt-6">
<Button variant="outline" onClick={() => setStep('upload')}>
Back
</Button>
<Button onClick={handleUpload} loading={loading}>
Create {data.length} QR Codes
</Button>
</div>
</CardContent>
</Card>
)}
{/* Complete Step */}
{step === 'complete' && (
<Card>
<CardContent className="p-12 text-center">
<div className="w-20 h-20 bg-success-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-10 h-10 text-success-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Upload Complete!</h2>
<p className="text-gray-600 mb-8">
Successfully created {data.length} QR codes
</p>
<div className="flex justify-center space-x-4">
<Button variant="outline" onClick={() => window.location.href = '/dashboard'}>
View Dashboard
</Button>
<Button onClick={() => {
setStep('upload');
setData([]);
setMapping({});
}}>
Upload More
</Button>
</div>
</CardContent>
</Card>
)}
</div>
);
}

View File

@ -26,7 +26,6 @@ export default function CreatePage() {
const [contentType, setContentType] = useState('URL');
const [content, setContent] = useState<any>({ url: '' });
const [isDynamic, setIsDynamic] = useState(true);
const [tags, setTags] = useState('');
// Style state
const [foregroundColor, setForegroundColor] = useState('#000000');
@ -61,8 +60,8 @@ export default function CreatePage() {
const contentTypes = [
{ value: 'URL', label: 'URL / Website' },
{ value: 'WIFI', label: 'WiFi Network' },
{ value: 'EMAIL', label: 'Email' },
{ value: 'VCARD', label: 'Contact Card' },
{ value: 'GEO', label: 'Location/Maps' },
{ value: 'PHONE', label: 'Phone Number' },
];
@ -73,12 +72,15 @@ export default function CreatePage() {
return content.url || 'https://example.com';
case 'PHONE':
return `tel:${content.phone || '+1234567890'}`;
case 'EMAIL':
return `mailto:${content.email || 'email@example.com'}${content.subject ? `?subject=${encodeURIComponent(content.subject)}` : ''}`;
case 'SMS':
return `sms:${content.phone || '+1234567890'}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`;
case 'WIFI':
return `WIFI:T:${content.security || 'WPA'};S:${content.ssid || 'NetworkName'};P:${content.password || ''};H:false;;`;
case 'VCARD':
return `BEGIN:VCARD\nVERSION:3.0\nFN:${content.firstName || 'John'} ${content.lastName || 'Doe'}\nORG:${content.organization || 'Company'}\nTITLE:${content.title || 'Position'}\nEMAIL:${content.email || 'email@example.com'}\nTEL:${content.phone || '+1234567890'}\nEND:VCARD`;
case 'GEO':
const lat = content.latitude || 37.7749;
const lon = content.longitude || -122.4194;
const label = content.label ? `?q=${encodeURIComponent(content.label)}` : '';
return `geo:${lat},${lon}${label}`;
case 'TEXT':
return content.text || 'Sample text';
case 'WHATSAPP':
@ -158,7 +160,7 @@ export default function CreatePage() {
contentType,
content,
isStatic: !isDynamic,
tags: tags.split(',').map(t => t.trim()).filter(Boolean),
tags: [],
style: {
// FREE users can only use black/white
foregroundColor: canCustomizeColors ? foregroundColor : '#000000',
@ -220,49 +222,76 @@ export default function CreatePage() {
required
/>
);
case 'EMAIL':
case 'VCARD':
return (
<>
<Input
label="First Name"
value={content.firstName || ''}
onChange={(e) => setContent({ ...content, firstName: e.target.value })}
placeholder="John"
required
/>
<Input
label="Last Name"
value={content.lastName || ''}
onChange={(e) => setContent({ ...content, lastName: e.target.value })}
placeholder="Doe"
required
/>
<Input
label="Email Address"
type="email"
value={content.email || ''}
onChange={(e) => setContent({ ...content, email: e.target.value })}
placeholder="contact@example.com"
required
placeholder="john@example.com"
/>
<Input
label="Subject (optional)"
value={content.subject || ''}
onChange={(e) => setContent({ ...content, subject: e.target.value })}
placeholder="Email subject"
label="Phone Number"
value={content.phone || ''}
onChange={(e) => setContent({ ...content, phone: e.target.value })}
placeholder="+1234567890"
/>
<Input
label="Company/Organization"
value={content.organization || ''}
onChange={(e) => setContent({ ...content, organization: e.target.value })}
placeholder="Company Name"
/>
<Input
label="Job Title"
value={content.title || ''}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="CEO"
/>
</>
);
case 'WIFI':
case 'GEO':
return (
<>
<Input
label="Network Name (SSID)"
value={content.ssid || ''}
onChange={(e) => setContent({ ...content, ssid: e.target.value })}
label="Latitude"
type="number"
step="any"
value={content.latitude || ''}
onChange={(e) => setContent({ ...content, latitude: parseFloat(e.target.value) || 0 })}
placeholder="37.7749"
required
/>
<Input
label="Password"
type="password"
value={content.password || ''}
onChange={(e) => setContent({ ...content, password: e.target.value })}
label="Longitude"
type="number"
step="any"
value={content.longitude || ''}
onChange={(e) => setContent({ ...content, longitude: parseFloat(e.target.value) || 0 })}
placeholder="-122.4194"
required
/>
<Select
label="Security"
value={content.security || 'WPA'}
onChange={(e) => setContent({ ...content, security: e.target.value })}
options={[
{ value: 'WPA', label: 'WPA/WPA2' },
{ value: 'WEP', label: 'WEP' },
{ value: 'nopass', label: 'No Password' },
]}
<Input
label="Location Label (optional)"
value={content.label || ''}
onChange={(e) => setContent({ ...content, label: e.target.value })}
placeholder="Golden Gate Bridge"
/>
</>
);
@ -318,13 +347,6 @@ export default function CreatePage() {
/>
{renderContentFields()}
<Input
label="Tags (comma-separated)"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="marketing, campaign, 2025"
/>
</CardContent>
</Card>

View File

@ -86,10 +86,10 @@ export default function DashboardPage() {
},
{
id: '5',
title: 'Contact Email',
title: 'Contact Card',
type: 'DYNAMIC' as const,
contentType: 'EMAIL',
slug: 'contact-email-qr',
contentType: 'VCARD',
slug: 'contact-card-qr',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:04:00Z',
scans: 0,
@ -255,7 +255,7 @@ export default function DashboardPage() {
}
try {
const response = await fetch(`/api/qrs/${id}`, {
const response = await fetchWithCsrf(`/api/qrs/${id}`, {
method: 'DELETE',
});

View File

@ -154,21 +154,75 @@ export default function EditQRPage() {
/>
)}
{qrCode.contentType === 'EMAIL' && (
{qrCode.contentType === 'VCARD' && (
<>
<Input
label="First Name"
value={content.firstName || ''}
onChange={(e) => setContent({ ...content, firstName: e.target.value })}
placeholder="John"
required
/>
<Input
label="Last Name"
value={content.lastName || ''}
onChange={(e) => setContent({ ...content, lastName: e.target.value })}
placeholder="Doe"
required
/>
<Input
label="Email"
type="email"
value={content.email || ''}
onChange={(e) => setContent({ ...content, email: e.target.value })}
placeholder="email@example.com"
placeholder="john@example.com"
/>
<Input
label="Phone"
value={content.phone || ''}
onChange={(e) => setContent({ ...content, phone: e.target.value })}
placeholder="+1234567890"
/>
<Input
label="Organization"
value={content.organization || ''}
onChange={(e) => setContent({ ...content, organization: e.target.value })}
placeholder="Company Name"
/>
<Input
label="Job Title"
value={content.title || ''}
onChange={(e) => setContent({ ...content, title: e.target.value })}
placeholder="CEO"
/>
</>
)}
{qrCode.contentType === 'GEO' && (
<>
<Input
label="Latitude"
type="number"
step="any"
value={content.latitude || ''}
onChange={(e) => setContent({ ...content, latitude: parseFloat(e.target.value) || 0 })}
placeholder="37.7749"
required
/>
<Input
label="Subject (Optional)"
value={content.subject || ''}
onChange={(e) => setContent({ ...content, subject: e.target.value })}
placeholder="Email subject"
label="Longitude"
type="number"
step="any"
value={content.longitude || ''}
onChange={(e) => setContent({ ...content, longitude: parseFloat(e.target.value) || 0 })}
placeholder="-122.4194"
required
/>
<Input
label="Location Label (Optional)"
value={content.label || ''}
onChange={(e) => setContent({ ...content, label: e.target.value })}
placeholder="Golden Gate Bridge"
/>
</>
)}

View File

@ -55,12 +55,6 @@ export default function LoginPage() {
window.location.href = '/api/auth/google';
};
// Demo login
const handleDemoLogin = () => {
setEmail('demo@qrmaster.com');
setPassword('demo123');
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
@ -149,18 +143,6 @@ export default function LoginPage() {
</svg>
Sign in with Google
</Button>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleDemoLogin}
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Use Demo Account
</Button>
</form>
<div className="mt-6 text-center">

View File

@ -144,193 +144,6 @@ const blogPosts: Record<string, BlogPostData> = {
<p>Whether you're deploying QR codes for event tracking, print marketing, bulk generation, or API-driven automation, scan analytics provides the data foundation for smarter, more effective campaigns. Start leveraging QR analytics today to unlock the full potential of your QR marketing strategy.</p>
</div>`,
},
'dynamische-vs-statische-qr-codes': {
slug: 'dynamische-vs-statische-qr-codes',
title: 'Dynamische vs. Statische QR-Codes: Der ultimative Vergleich',
excerpt: 'Entdecken Sie die wichtigsten Unterschiede zwischen dynamischen und statischen QR-Codes und wählen Sie die richtige Option für Ihre Kampagne.',
date: 'October 15, 2025',
datePublished: '2025-10-15T09:00:00Z',
dateModified: '2025-10-15T09:00:00Z',
readTime: '6 Min',
category: 'Grundlagen',
image: 'https://images.unsplash.com/photo-1603791440384-56cd371ee9a7?w=1200&q=80',
imageAlt: 'Vergleich zwischen dynamischen und statischen QR-Codes mit Diagrammen',
author: 'QR Master Team',
authorUrl: 'https://www.qrmaster.com/about',
content: `<div class="blog-content">
<h2>Was sind statische QR-Codes?</h2>
<p>Statische QR-Codes enthalten fest eingebettete Informationen, die nach der Erstellung nicht mehr geändert werden können. Der QR-Code speichert die Daten direkt zum Beispiel eine URL, einen Text oder Kontaktdaten. Sobald der Code gedruckt ist, bleibt sein Inhalt permanent.</p>
<h3>Vorteile statischer QR-Codes</h3>
<p><strong>Für immer gültig:</strong> Statische QR-Codes funktionieren unabhängig von Servern oder Abonnements. Sie sind ideal für Anwendungen, bei denen Sie garantiert langfristige Verfügbarkeit benötigen.</p>
<p><strong>Keine laufenden Kosten:</strong> Da keine Server-Infrastruktur erforderlich ist, fallen keine monatlichen Gebühren an.</p>
<p><strong>Schneller Scan:</strong> Direkter Zugriff auf Inhalte ohne Umleitung über Server.</p>
<h3>Nachteile statischer QR-Codes</h3>
<p><strong>Nicht editierbar:</strong> Nach dem Druck können Sie die verlinkten Inhalte nicht mehr ändern.</p>
<p><strong>Keine Analytics:</strong> Sie können nicht nachverfolgen, wie oft der Code gescannt wurde.</p>
<p><strong>Größer bei langen URLs:</strong> Je mehr Daten eingebettet sind, desto komplexer und größer wird der QR-Code.</p>
<h2>Was sind dynamische QR-Codes?</h2>
<p>Dynamische QR-Codes enthalten eine kurze Weiterleitungs-URL, die auf einen Server verweist. Der Server speichert die eigentliche Ziel-URL. Das bedeutet, dass Sie die Ziel-URL jederzeit ändern können, ohne den gedruckten QR-Code neu erstellen zu müssen.</p>
<h3>Vorteile dynamischer QR-Codes</h3>
<p><strong>Editierbar:</strong> Ändern Sie die Ziel-URL jederzeit, auch nach dem Druck.</p>
<p><strong>Detaillierte Analytics:</strong> Verfolgen Sie Scans nach Standort, Gerät, Zeit und mehr.</p>
<p><strong>Retargeting-fähig:</strong> Nutzen Sie Scan-Daten für personalisierte Marketing-Kampagnen.</p>
<p><strong>Kompakter Code:</strong> Da nur eine kurze Umleitungs-URL eingebettet ist, bleibt der QR-Code kleiner und einfacher zu scannen.</p>
<h3>Nachteile dynamischer QR-Codes</h3>
<p><strong>Erfordert Abo:</strong> Dynamische QR-Codes benötigen eine aktive Server-Infrastruktur, was meist mit monatlichen Kosten verbunden ist.</p>
<p><strong>Abhängig vom Server:</strong> Wenn der Server ausfällt oder das Abo abläuft, funktioniert der QR-Code nicht mehr.</p>
<h2>Vergleichstabelle: Statisch vs. Dynamisch</h2>
<div class="overflow-x-auto my-8">
<table class="min-w-full border-collapse border border-gray-300">
<thead class="bg-gray-100">
<tr>
<th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Feature</th>
<th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Statisch</th>
<th class="border border-gray-300 px-6 py-3 text-left font-semibold text-gray-900">Dynamisch</th>
</tr>
</thead>
<tbody>
<tr class="bg-white">
<td class="border border-gray-300 px-6 py-4 font-medium text-gray-900">Editierbar</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Nein</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Ja</td>
</tr>
<tr class="bg-gray-50">
<td class="border border-gray-300 px-6 py-4 font-medium text-gray-900">Analytics</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Nein</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Ja</td>
</tr>
<tr class="bg-white">
<td class="border border-gray-300 px-6 py-4 font-medium text-gray-900">Kosten</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Kostenlos</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Abo erforderlich</td>
</tr>
<tr class="bg-gray-50">
<td class="border border-gray-300 px-6 py-4 font-medium text-gray-900">Größe</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Größer bei langen URLs</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Immer kompakt</td>
</tr>
<tr class="bg-white">
<td class="border border-gray-300 px-6 py-4 font-medium text-gray-900">Gültigkeitsdauer</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Für immer</td>
<td class="border border-gray-300 px-6 py-4 text-gray-700">Abhängig vom Abo</td>
</tr>
</tbody>
</table>
</div>
<h2>Wann sollten Sie welchen QR-Code-Typ verwenden?</h2>
<h3>Verwenden Sie statische QR-Codes für:</h3>
<p> Visitenkarten mit festen Kontaktdaten</p>
<p> WLAN-Passwörter</p>
<p> Produktverpackungen mit permanenten URLs</p>
<p> Anwendungen, bei denen Sie keine Analytics benötigen</p>
<h3>Verwenden Sie dynamische QR-Codes für:</h3>
<p> Marketing-Kampagnen mit wechselnden Angeboten</p>
<p> Event-Tickets mit aktualisierbaren Informationen</p>
<p> Print-Anzeigen, bei denen Sie die Landingpage optimieren möchten</p>
<p> Jede Anwendung, bei der Sie Scan-Statistiken tracken möchten</p>
<h2>Fazit</h2>
<p>Statische QR-Codes sind ideal für permanente, unveränderliche Inhalte ohne Tracking-Bedarf. Dynamische QR-Codes bieten Flexibilität, Analytics und Marketing-Power perfekt für professionelle Kampagnen. Wählen Sie basierend auf Ihren Anforderungen: Langfristige Stabilität oder Marketing-Flexibilität?</p>
</div>`,
},
'qr-codes-im-restaurant': {
slug: 'qr-codes-im-restaurant',
title: 'QR-Codes im Restaurant: Digitale Speisekarten & kontaktloses Bestellen',
excerpt: 'Erfahren Sie, wie Restaurants QR-Codes für digitale Menüs, kontaktlose Bestellungen und verbessertes Gästeerlebnis einsetzen.',
date: 'October 14, 2025',
datePublished: '2025-10-14T09:00:00Z',
dateModified: '2025-10-14T09:00:00Z',
readTime: '7 Min',
category: 'Anwendungsfälle',
image: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=1200&q=80',
imageAlt: 'Restaurant-Tisch mit QR-Code für digitale Speisekarte',
author: 'QR Master Team',
authorUrl: 'https://www.qrmaster.com/about',
content: `<div class="blog-content">
<h2>Warum QR-Codes für Restaurants?</h2>
<p>Seit der Pandemie haben QR-Codes die Gastronomie revolutioniert. Sie ermöglichen kontaktloses Bestellen, reduzieren Druckkosten für Speisekarten und verbessern das Gästeerlebnis durch interaktive Funktionen. Moderne Gäste erwarten digitale Lösungen QR-Codes liefern genau das.</p>
<h2>Digitale Speisekarten mit QR-Codes</h2>
<h3>Vorteile für Restaurants</h3>
<p><strong>Kosten sparen:</strong> Keine teuren Speisekarten-Neudrucke bei Preisänderungen oder neuen Gerichten.</p>
<p><strong>Aktualisierungen in Echtzeit:</strong> Passen Sie Ihr Menü sofort an täglich wechselnde Angebote, Verfügbarkeiten oder Allergiehinweise.</p>
<p><strong>Mehrsprachigkeit:</strong> Bieten Sie Ihre Speisekarte automatisch in mehreren Sprachen an.</p>
<p><strong>Hygiene:</strong> Gäste scannen mit ihrem eigenen Smartphone keine gemeinsam genutzten Speisekarten mehr.</p>
<h3>Vorteile für Gäste</h3>
<p> Sofortiger Zugriff auf die Speisekarte ohne Warten</p>
<p> Hochauflösende Bilder der Gerichte</p>
<p> Detaillierte Nährwertangaben und Allergiehinweise</p>
<p> Personalisierte Empfehlungen</p>
<h2>Kontaktloses Bestellen via QR-Code</h2>
<p>QR-Codes ermöglichen es Gästen, direkt vom Tisch aus zu bestellen ohne auf Kellner zu warten. Das System ist einfach: Gast scannt Code wählt Gerichte aus bestätigt Bestellung zahlt digital.</p>
<h3>Vorteile des kontaktlosen Bestellens</h3>
<p><strong>Schnellerer Service:</strong> Bestellungen gehen direkt in die Küche, ohne Zwischenschritte.</p>
<p><strong>Weniger Fehler:</strong> Gäste geben ihre Bestellung selbst ein keine Missverständnisse mehr.</p>
<p><strong>Upselling-Möglichkeiten:</strong> Zeigen Sie automatisch passende Empfehlungen oder Zusatzprodukte an.</p>
<p><strong>Höhere Tischrotation:</strong> Weniger Wartezeiten bedeuten mehr bediente Gäste pro Abend.</p>
<h2>QR-Code-Platzierung im Restaurant</h2>
<h3>Auf dem Tisch</h3>
<p>Der klassische Ansatz: QR-Codes auf Tischaufstellern, Tischsets oder direkt auf dem Tisch. Ideal für Restaurants mit festem Sitzplatz.</p>
<h3>An der Wand oder am Eingang</h3>
<p>Für Schnellrestaurants oder Cafés: Gäste scannen beim Betreten und bestellen an der Theke oder am Tisch.</p>
<h3>Auf Rechnungen und Flyern</h3>
<p>Nutzen Sie QR-Codes auf Rechnungen für Online-Bewertungen, Treueprogramme oder Social-Media-Verlinkungen.</p>
<h2>Best Practices für Restaurant-QR-Codes</h2>
<h3>1. Design: Markenkonform und ansprechend</h3>
<p>Gestalten Sie QR-Codes in Ihren Markenfarben mit Ihrem Logo. Ein professionelles Design erhöht die Scan-Rate.</p>
<h3>2. Klare Anweisungen</h3>
<p>Fügen Sie einen Call-to-Action hinzu: "Scannen für Speisekarte" oder "Hier bestellen". Nicht jeder Gast ist mit QR-Codes vertraut.</p>
<h3>3. Mobile Optimierung</h3>
<p>Ihre digitale Speisekarte MUSS mobilfreundlich sein. Große Schriftarten, einfache Navigation, schnelle Ladezeiten.</p>
<h3>4. Testen Sie regelmäßig</h3>
<p>Prüfen Sie wöchentlich, ob alle QR-Codes funktionieren und zur richtigen Seite führen.</p>
<h2>Analytics: Messen Sie den Erfolg</h2>
<p>Mit dynamischen QR-Codes erhalten Sie wertvolle Insights:</p>
<p> Wie viele Gäste scannen den QR-Code?</p>
<p> Welche Gerichte werden am häufigsten angesehen?</p>
<p> Zu welchen Uhrzeiten ist die Nachfrage am höchsten?</p>
<p> Welche Tische haben die höchste Scan-Rate?</p>
<h2>Praxisbeispiel: Pizza-Restaurant "Bella Italia"</h2>
<p>Das fiktive Restaurant "Bella Italia" führte QR-Code-Bestellungen ein und erzielte innerhalb von 3 Monaten:</p>
<p> 40% schnellere Bestellabwicklung</p>
<p> 25% höherer Umsatz durch Upselling-Vorschläge</p>
<p> 90% der Gäste bevorzugen QR-Bestellungen gegenüber klassischer Bedienung</p>
<p> 50% Kosteneinsparung bei Speisekarten-Druck</p>
<h2>Häufige Fragen (FAQ)</h2>
<h3>Was, wenn Gäste kein Smartphone haben?</h3>
<p>Halten Sie einige gedruckte Speisekarten als Backup bereit besonders für ältere Gäste.</p>
<h3>Wie sicher ist das Bezahlen via QR-Code?</h3>
<p>Nutzen Sie etablierte Payment-Anbieter wie Stripe oder PayPal, die höchste Sicherheitsstandards erfüllen.</p>
<h3>Brauche ich technisches Know-how?</h3>
<p>Nein! Plattformen wie QR Master ermöglichen die Erstellung und Verwaltung von Restaurant-QR-Codes ohne Programmierkenntnisse.</p>
<h2>Fazit</h2>
<p>QR-Codes sind die Zukunft der Gastronomie. Sie verbessern das Gästeerlebnis, sparen Kosten und steigern den Umsatz. Egal ob kleines Café oder gehobenes Restaurant QR-Codes lohnen sich für jeden Betrieb. Starten Sie noch heute mit Ihrer digitalen Transformation!</p>
</div>`,
},
'qr-code-tracking-guide-2025': {
slug: 'qr-code-tracking-guide-2025',
title: 'QR Code Tracking: Complete Guide 2025 (Free Tools & Best Practices)',
@ -868,7 +681,7 @@ app.get('/qr/:id', async (req, res) => {
imageAlt: 'Two QR codes side by side showing static and dynamic comparison',
author: 'QR Master Team',
authorUrl: 'https://www.qrmaster.com/about',
answer: 'Static QR codes encode data directly and cannot be edited after creation, while dynamic QR codes contain a short redirect URL that can be updated anytime. Dynamic QR codes also provide tracking analytics, making them ideal for marketing campaigns. Static QR codes work forever without subscriptions, perfect for permanent content like WiFi passwords or fixed URLs.',
answer: 'Static QR codes encode data directly and cannot be edited after creation, while dynamic QR codes contain a short redirect URL that can be updated anytime. Dynamic QR codes also provide tracking analytics, making them ideal for marketing campaigns. Static QR codes work forever without subscriptions, perfect for permanent content like contact cards or fixed URLs.',
content: `<div class="blog-content">
<p>Choosing between static and dynamic QR codes is one of the most important decisions when implementing a QR code strategy. According to <a href="https://en.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener">Wikipedia</a>, QR codes were invented in 1994 by Masahiro Hara at Denso Wave for automotive part tracking. Today, QR codes have evolved into sophisticated marketing tools, with dynamic QR codes offering features unimaginable in their original static form.</p>
@ -879,14 +692,14 @@ app.get('/qr/:id', async (req, res) => {
<p>A static QR code directly encodes your data into the QR code pattern itself. When you create a static QR code for a URL, that URL is permanently embedded in the black-and-white squares. The QR code reader decodes the pattern and accesses the content directlyno intermediate server, no redirect, no tracking.</p>
<h3>How Static QR Codes Work</h3>
<p>Think of a static QR code like printing a phone number on a business card. The phone number is the final informationthere's no lookup service or translation layer. When someone scans the QR code, their device reads the encoded data and immediately processes it (opens the URL, displays the text, connects to WiFi, etc.).</p>
<p>Think of a static QR code like printing a phone number on a business card. The phone number is the final informationthere's no lookup service or translation layer. When someone scans the QR code, their device reads the encoded data and immediately processes it (opens the URL, displays the text, opens a location in maps, etc.).</p>
<p><strong>Example:</strong> If you create a static QR code for <code>https://www.yourwebsite.com/summer-sale-2025</code>, that exact URL is encoded into the QR code pattern. The QR code scanner extracts this URL and opens it directly.</p>
<h3>Common Uses for Static QR Codes</h3>
<ul>
<li><strong>WiFi passwords:</strong> Encode network credentials for guest access</li>
<li><strong>Business card vCards:</strong> Share permanent contact information</li>
<li><strong>Contact cards (vCard):</strong> Share permanent contact information on business cards</li>
<li><strong>Location links:</strong> Direct links to Google Maps locations for offices or stores</li>
<li><strong>App store links:</strong> Fixed URLs that never change</li>
<li><strong>Bitcoin wallet addresses:</strong> Cryptocurrency payment addresses</li>
<li><strong>Fixed website URLs:</strong> Company homepage, about page, etc.</li>
@ -898,7 +711,7 @@ app.get('/qr/:id', async (req, res) => {
<ul>
<li><strong> Works forever:</strong> No dependency on external servers or subscriptions. Once created, it functions permanently.</li>
<li><strong> Faster scanning:</strong> No redirect delayscanner goes directly to content (typically 100-300ms faster than dynamic).</li>
<li><strong> Works offline:</strong> For content types like WiFi credentials or vCards, no internet connection needed.</li>
<li><strong> Works offline:</strong> For content types like vCards or location data, no internet connection needed for initial scan.</li>
<li><strong> Completely free:</strong> No ongoing costs or subscriptions required.</li>
<li><strong> Privacy-friendly:</strong> No tracking, no data collection, no third-party involvement.</li>
<li><strong> Simple:</strong> What you encode is what you getno complexity.</li>
@ -1077,9 +890,9 @@ Tracking ✓ | Editable ✓ | Analytics ✓
<h3>When Static QR Codes Excel</h3>
<h4>Scenario 1: Coffee Shop WiFi Password</h4>
<p><strong>Situation:</strong> Coffee shop wants customers to easily connect to WiFi.</p>
<p><strong>Why Static:</strong> WiFi credentials never change. No tracking needed. QR code works forever even if you stop paying for QR service. Privacy-friendly (no data collection).</p>
<h4>Scenario 1: Business Card Contact Information</h4>
<p><strong>Situation:</strong> Professional wants to share their contact details easily at networking events.</p>
<p><strong>Why Static:</strong> Contact information rarely changes. No tracking needed for personal cards. QR code works forever even if you stop paying for QR service. Privacy-friendly (no data collection).</p>
<p><strong>Cost Savings:</strong> $0 forever vs $5-15/month for dynamic QR service = $60-180/year saved.</p>
<h4>Scenario 2: Book Back Cover</h4>
@ -1111,7 +924,7 @@ Tracking ✓ | Editable ✓ | Analytics ✓
<h3>1. Content Never Changes</h3>
<ul>
<li>WiFi password that's permanent</li>
<li>Contact information (vCard) that remains constant</li>
<li>App store download link (Apple App Store / Google Play URLs are stable)</li>
<li>Company homepage that's been the same for years</li>
<li>Historical information (museum exhibits, memorial plaques)</li>
@ -1135,8 +948,8 @@ Tracking ✓ | Editable ✓ | Analytics ✓
<h3>4. Offline Content</h3>
<ul>
<li>WiFi credentials (works without internet)</li>
<li>vCard contact information (stored locally on device)</li>
<li>Location coordinates (opens maps app directly)</li>
<li>Plain text messages or instructions</li>
<li>SMS or phone number links</li>
</ul>

View File

@ -3,9 +3,10 @@ import type { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import SeoJsonLd from '@/components/SeoJsonLd';
import { websiteSchema } from '@/lib/schema';
import { websiteSchema, breadcrumbSchema } from '@/lib/schema';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
function truncateAtWord(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
@ -84,11 +85,17 @@ const blogPosts = [
];
export default function BlogPage() {
const breadcrumbItems: BreadcrumbItem[] = [
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
];
return (
<>
<SeoJsonLd data={websiteSchema()} />
<SeoJsonLd data={[websiteSchema(), breadcrumbSchema(breadcrumbItems)]} />
<div className="py-20 bg-gradient-to-b from-gray-50 to-white">
<div className="container mx-auto px-4">
<Breadcrumbs items={breadcrumbItems} />
<div className="text-center mb-16">
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6">
QR Code Insights

View File

@ -3,19 +3,77 @@ import type { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import SeoJsonLd from '@/components/SeoJsonLd';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel | QR Master',
description: 'Generate hundreds of QR codes at once from CSV or Excel files. 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',
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Create URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory management.',
keywords: 'bulk qr code generator, batch qr code, qr code from excel, csv qr code generator, mass qr code generation, bulk vcard qr code, bulk qr codes free',
alternates: {
canonical: 'https://www.qrmaster.com/bulk-qr-code-generator',
languages: {
'x-default': 'https://www.qrmaster.com/bulk-qr-code-generator',
en: 'https://www.qrmaster.com/bulk-qr-code-generator',
},
},
openGraph: {
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
url: 'https://www.qrmaster.com/bulk-qr-code-generator',
type: 'website',
},
twitter: {
title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
},
};
export default function BulkQRCodeGeneratorPage() {
const qrCodeTypes = [
{
type: 'URL',
icon: '🌐',
title: 'Website Links',
description: 'Generate QR codes for websites, landing pages, and online content',
format: 'https://example.com/product',
example: 'Product Page,URL,https://example.com/product',
},
{
type: 'VCARD',
icon: '👤',
title: 'Contact Cards',
description: 'Create vCard QR codes with contact information',
format: 'FirstName,LastName,Email,Phone,Organization,Title',
example: 'John Doe,VCARD,John,Doe,john@example.com,+1234567890,Company Inc,CEO',
},
{
type: 'GEO',
icon: '📍',
title: 'Locations',
description: 'Generate location QR codes with GPS coordinates',
format: 'latitude,longitude,label',
example: 'Office Location,GEO,37.7749,-122.4194,Main Office',
},
{
type: 'PHONE',
icon: '📞',
title: 'Phone Numbers',
description: 'Create QR codes that dial phone numbers',
format: '+1234567890',
example: 'Support Hotline,PHONE,+1234567890',
},
{
type: 'TEXT',
icon: '📝',
title: 'Plain Text',
description: 'Generate QR codes with any text content',
format: 'Your text content here',
example: 'Serial Number,TEXT,SN-12345-ABCDE',
},
];
const bulkFeatures = [
{
icon: '📊',
@ -110,278 +168,494 @@ export default function BulkQRCodeGeneratorPage() {
{ column: 'tags', description: 'Comma-separated tags', required: false },
];
const softwareSchema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
'@id': 'https://www.qrmaster.com/bulk-qr-code-generator#software',
name: 'QR Master - Bulk QR Code Generator',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '980',
},
description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, inventory management with custom branding.',
featureList: [
'Excel and CSV file import',
'Generate up to 1000 QR codes at once',
'Unified branding and design',
'Batch download as ZIP',
'Individual tracking per code',
'Bulk update capabilities',
'Custom filenames',
'High-resolution exports',
],
};
const howToSchema = {
'@context': 'https://schema.org',
'@type': 'HowTo',
'@id': 'https://www.qrmaster.com/bulk-qr-code-generator#howto',
name: 'How to Generate Bulk QR Codes from Excel',
description: 'Learn how to create hundreds of QR codes from an Excel or CSV file',
totalTime: 'PT10M',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Prepare Excel File',
text: 'Create an Excel or CSV file with columns for name, URL, and any custom data you need',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Upload File',
text: 'Log into QR Master and upload your file to the bulk generator',
url: 'https://www.qrmaster.com/bulk-creation',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Map Columns',
text: 'Map your file columns to QR code fields (name, URL, description, etc.)',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Customize Design',
text: 'Apply your logo, brand colors, and design settings to all QR codes at once',
},
{
'@type': 'HowToStep',
position: 5,
name: 'Generate and Download',
text: 'Click generate and download all QR codes as a ZIP file with custom filenames',
},
],
};
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': 'https://www.qrmaster.com/bulk-qr-code-generator#faq',
mainEntity: [
{
'@type': 'Question',
name: 'How many QR codes can I generate at once?',
acceptedAnswer: {
'@type': 'Answer',
text: 'With QR Master, you can generate up to 1000 QR codes at once from a CSV or Excel file. For larger volumes, contact our enterprise team.',
},
},
{
'@type': 'Question',
name: 'What file formats are supported?',
acceptedAnswer: {
'@type': 'Answer',
text: 'QR Master supports CSV (.csv), Excel (.xlsx, .xls), and other spreadsheet formats. Simply ensure your file has columns for name and destination URL.',
},
},
{
'@type': 'Question',
name: 'Can I apply my branding to all QR codes?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, you can apply your logo, brand colors, and custom design to all QR codes in your bulk generation. All codes will have consistent branding.',
},
},
{
'@type': 'Question',
name: 'Are bulk generated QR codes trackable?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, each QR code generated in bulk is individually trackable. You can see scans, locations, and analytics for every single code in your dashboard.',
},
},
],
};
const breadcrumbItems: BreadcrumbItem[] = [
{ name: 'Home', url: '/' },
{ name: 'Bulk QR Code Generator', url: '/bulk-qr-code-generator' },
];
return (
<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>
<>
<SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} />
<div className="min-h-screen bg-white">
{/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-to-br from-green-50 via-white to-blue-50 py-20">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<Breadcrumbs items={breadcrumbItems} />
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div className="space-y-8">
<div className="inline-flex items-center space-x-2 bg-green-100 text-green-800 px-4 py-2 rounded-full text-sm font-semibold">
<span></span>
<span>Generate 1000s in Minutes</span>
</div>
<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}
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
Bulk QR Code Generator
</h1>
<p className="text-xl text-gray-600 leading-relaxed">
Create hundreds or thousands of QR codes from Excel or CSV files. Generate URLs, vCards, locations, phone numbers, and text QR codes in bulk. Perfect for products, events, inventory, and marketing campaigns.
</p>
<div className="space-y-3">
{[
'Upload Excel or CSV files',
'Generate URLs, vCards, locations & more',
'Custom branding on all codes',
'Download as organized ZIP',
].map((feature, index) => (
<div key={index} className="flex items-center space-x-3">
<div className="flex-shrink-0 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-gray-700">{feature}</span>
</div>
))}
</div>
<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 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>
</div>
</section>
</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>
{/* Supported QR Code Types */}
<section className="py-20">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Supported QR Code Types
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Generate multiple types of QR codes from your CSV or Excel file. Each type has its own format requirements.
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-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 className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{qrCodeTypes.map((qrType, index) => (
<Card key={index} className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-center space-x-3 mb-4">
<div className="text-3xl">{qrType.icon}</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{qrType.title}
</h3>
<span className="text-xs font-mono text-gray-500">{qrType.type}</span>
</div>
</div>
</div>
</Card>
))}
</div>
</div>
</section>
<p className="text-gray-600 mb-4 text-sm">
{qrType.description}
</p>
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<p className="text-xs font-semibold text-gray-700 mb-1">Format:</p>
<code className="text-xs text-gray-900 break-all">{qrType.format}</code>
</div>
<div className="bg-blue-50 rounded-lg p-3">
<p className="text-xs font-semibold text-blue-700 mb-1">CSV Example:</p>
<code className="text-xs text-blue-900 break-all">{qrType.example}</code>
</div>
</Card>
))}
</div>
{/* 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 className="mt-12 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-8">
<h3 className="text-xl font-bold text-gray-900 mb-4 text-center">
📥 CSV/Excel File Format
</h3>
<p className="text-gray-600 text-center mb-6">
Your file must have at least these three columns: <code className="bg-white px-2 py-1 rounded">title</code>, <code className="bg-white px-2 py-1 rounded">contentType</code>, and <code className="bg-white px-2 py-1 rounded">content</code>
</p>
<div className="bg-white rounded-lg p-6 shadow-sm overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b-2 border-gray-300">
<th className="text-left py-2 px-3 font-semibold text-gray-700">title</th>
<th className="text-left py-2 px-3 font-semibold text-gray-700">contentType</th>
<th className="text-left py-2 px-3 font-semibold text-gray-700">content</th>
<th className="text-left py-2 px-3 font-semibold text-gray-700">tags</th>
</tr>
</thead>
<tbody className="font-mono text-xs">
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Product Page</td>
<td className="py-2 px-3">URL</td>
<td className="py-2 px-3">https://example.com/product</td>
<td className="py-2 px-3">product,shop</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">John Doe</td>
<td className="py-2 px-3">VCARD</td>
<td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td>
<td className="py-2 px-3">contact</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Office Location</td>
<td className="py-2 px-3">GEO</td>
<td className="py-2 px-3">37.7749,-122.4194,Main Office</td>
<td className="py-2 px-3">location</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Support Hotline</td>
<td className="py-2 px-3">PHONE</td>
<td className="py-2 px-3">+1234567890</td>
<td className="py-2 px-3">support</td>
</tr>
<tr>
<td className="py-2 px-3">Serial Number</td>
<td className="py-2 px-3">TEXT</td>
<td className="py-2 px-3">SN-12345-ABCDE</td>
<td className="py-2 px-3">product,serial</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<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>
{/* 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>
))}
</tbody>
</table>
</Card>
</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
<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>
</pre>
</Card>
</div>
</section>
<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>
{/* 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>
</div>
</Card>
))}
</Card>
))}
</div>
</div>
</div>
</section>
</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>
{/* 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>
</div>
</section>
</div>
</section>
</div>
</>
);
}

View File

@ -3,16 +3,31 @@ import type { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import SeoJsonLd from '@/components/SeoJsonLd';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
description: 'Create dynamic QR codes that can be edited after printing. Change destination URL, track scans, and update content without reprinting. Free dynamic QR code generator.',
keywords: 'dynamic qr code generator, editable qr code, dynamic qr code, free dynamic qr code, qr code generator dynamic, best dynamic qr code generator',
alternates: {
canonical: 'https://www.qrmaster.com/dynamic-qr-code-generator',
languages: {
'x-default': 'https://www.qrmaster.com/dynamic-qr-code-generator',
en: 'https://www.qrmaster.com/dynamic-qr-code-generator',
},
},
openGraph: {
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
url: 'https://www.qrmaster.com/dynamic-qr-code-generator',
type: 'website',
},
twitter: {
title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
},
};
export default function DynamicQRCodeGeneratorPage() {
@ -114,280 +129,390 @@ export default function DynamicQRCodeGeneratorPage() {
},
];
const softwareSchema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
'@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#software',
name: 'QR Master - Dynamic QR Code Generator',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.9',
ratingCount: '2150',
},
description: 'Create dynamic QR codes that can be edited after printing. Change destination URLs, track scans, and update content without reprinting.',
featureList: [
'Edit QR codes after printing',
'Real-time scan tracking',
'A/B testing capabilities',
'Custom branding and design',
'Geo-targeting options',
'Scheduled content updates',
'Password protection',
'Expiration dates',
],
};
const howToSchema = {
'@context': 'https://schema.org',
'@type': 'HowTo',
'@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#howto',
name: 'How to Create a Dynamic QR Code',
description: 'Learn how to create editable QR codes that can be updated after printing',
totalTime: 'PT3M',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Sign Up Free',
text: 'Create a free QR Master account to start generating dynamic QR codes',
url: 'https://www.qrmaster.com/signup',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Generate QR Code',
text: 'Enter your destination URL and customize the design with your branding',
url: 'https://www.qrmaster.com/create',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Download and Print',
text: 'Download your QR code in high resolution and add it to your marketing materials',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Update Anytime',
text: 'Log into your dashboard to change the destination URL whenever needed - no reprinting required',
url: 'https://www.qrmaster.com/dashboard',
},
],
};
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#faq',
mainEntity: [
{
'@type': 'Question',
name: 'What is a dynamic QR code?',
acceptedAnswer: {
'@type': 'Answer',
text: 'A dynamic QR code is an editable QR code that redirects through a short URL, allowing you to change the destination without reprinting the code. Unlike static QR codes, dynamic codes can be tracked and updated anytime.',
},
},
{
'@type': 'Question',
name: 'Can I edit a QR code after printing?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, with dynamic QR codes you can edit the destination URL anytime after printing. The QR code image stays the same, but the content it points to can be changed from your dashboard.',
},
},
{
'@type': 'Question',
name: 'Is dynamic QR code generator free?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes, QR Master offers a free plan for creating dynamic QR codes with basic tracking features. Premium plans include advanced analytics and customization options.',
},
},
],
};
const breadcrumbItems: BreadcrumbItem[] = [
{ name: 'Home', url: '/' },
{ name: 'Dynamic QR Code Generator', url: '/dynamic-qr-code-generator' },
];
return (
<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>
<>
<SeoJsonLd data={[softwareSchema, howToSchema, faqSchema, breadcrumbSchema(breadcrumbItems)]} />
<div className="min-h-screen bg-white">
{/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-to-br from-purple-50 via-white to-blue-50 py-20">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<Breadcrumbs items={breadcrumbItems} />
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div className="space-y-8">
<div className="inline-flex items-center space-x-2 bg-purple-100 text-purple-800 px-4 py-2 rounded-full text-sm font-semibold">
<span></span>
<span>Edit After Printing</span>
</div>
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
Dynamic QR Code Generator
</h1>
<p className="text-xl text-gray-600 leading-relaxed">
Create QR codes you can edit anytime - even after printing. Change URLs, track scans, and update content without reprinting. The smart choice for businesses.
</p>
<div className="space-y-3">
{[
'Edit content after printing',
'Track scans and analytics',
'A/B test without reprinting',
'Custom branding and design',
].map((feature, index) => (
<div key={index} className="flex items-center space-x-3">
<div className="flex-shrink-0 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-gray-700">{feature}</span>
</div>
))}
</div>
<div className="flex flex-col sm:flex-row gap-4">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Create Dynamic QR Code
</Button>
</Link>
<Link href="/pricing">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
View Pricing
</Button>
</Link>
</div>
</div>
<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" />
{/* 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>
<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 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 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>
</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>
{/* 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>
</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>
<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>
<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>
))}
{/* 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>
</div>
</section>
</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>
{/* 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}
<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>
))}
</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
<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 text-gray-600">
Simple technology, powerful results
<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>
<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>
</section>
</div>
</>
);
}

View File

@ -4,7 +4,6 @@ import React, { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import CookieBanner from '@/components/CookieBanner';
import en from '@/i18n/en.json';
export default function MarketingLayout({
@ -33,7 +32,7 @@ export default function MarketingLayout({
<div className="flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2">
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
<img src="/favicon.svg" alt="QR Master" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900">QR Master</span>
</Link>
@ -111,7 +110,7 @@ export default function MarketingLayout({
<div className="grid md:grid-cols-4 gap-8">
<div>
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity">
<img src="/favicon.svg" alt="QR Master" className="w-10 h-10" />
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
<span className="text-xl font-bold">QR Master</span>
</Link>
<p className="text-gray-400">
@ -138,14 +137,6 @@ export default function MarketingLayout({
<li><Link href="/signup" className="hover:text-white">Get Started</Link></li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4">Legal</h3>
<ul className="space-y-2 text-gray-400">
<li><Link href="/privacy" className="hover:text-white">Privacy Policy</Link></li>
<li><Link href="/terms" className="hover:text-white">Terms of Service</Link></li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
@ -153,9 +144,6 @@ export default function MarketingLayout({
</div>
</div>
</footer>
{/* Cookie Banner */}
<CookieBanner />
</div>
);
}

View File

@ -3,16 +3,31 @@ import type { Metadata } from 'next';
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import SeoJsonLd from '@/components/SeoJsonLd';
import Breadcrumbs, { BreadcrumbItem } from '@/components/Breadcrumbs';
import { breadcrumbSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior. Free QR code tracking software with detailed reports.',
keywords: 'qr code tracking, qr code analytics, track qr scans, qr code statistics, free qr tracking, qr code monitoring',
alternates: {
canonical: 'https://www.qrmaster.com/qr-code-tracking',
languages: {
'x-default': 'https://www.qrmaster.com/qr-code-tracking',
en: 'https://www.qrmaster.com/qr-code-tracking',
},
},
openGraph: {
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
url: 'https://www.qrmaster.com/qr-code-tracking',
type: 'website',
},
twitter: {
title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master',
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.',
},
};
export default function QRCodeTrackingPage() {
@ -82,225 +97,302 @@ export default function QRCodeTrackingPage() {
{ feature: 'API Access', free: false, qrMaster: true },
];
const softwareSchema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
'@id': 'https://www.qrmaster.com/qr-code-tracking#software',
name: 'QR Master - QR Code Tracking & Analytics',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web Browser, iOS, Android',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '1250',
},
description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior with our free QR code tracking software.',
features: [
'Real-time analytics dashboard',
'Location tracking by country and city',
'Device detection (iOS, Android, Desktop)',
'Time-based scan reports',
'Unique vs total scan tracking',
'Campaign performance metrics',
'Unlimited scans',
'Export detailed reports',
],
};
const howToSchema = {
'@context': 'https://schema.org',
'@type': 'HowTo',
'@id': 'https://www.qrmaster.com/qr-code-tracking#howto',
name: 'How to Track QR Code Scans',
description: 'Learn how to track and analyze QR code scans with real-time analytics',
totalTime: 'PT5M',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Create QR Code',
text: 'Sign up for free and create a dynamic QR code with tracking enabled',
url: 'https://www.qrmaster.com/signup',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Deploy QR Code',
text: 'Download and place your QR code on marketing materials, products, or digital platforms',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Monitor Analytics',
text: 'View real-time scan data including location, device, and time patterns in your dashboard',
url: 'https://www.qrmaster.com/analytics',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Optimize Campaigns',
text: 'Use insights to optimize placement, timing, and targeting of your QR code campaigns',
},
],
};
const breadcrumbItems: BreadcrumbItem[] = [
{ name: 'Home', url: '/' },
{ name: 'QR Code Tracking', url: '/qr-code-tracking' },
];
return (
<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>
<>
<SeoJsonLd data={[softwareSchema, howToSchema, breadcrumbSchema(breadcrumbItems)]} />
<div className="min-h-screen bg-white">
{/* Hero Section */}
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<Breadcrumbs items={breadcrumbItems} />
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div className="space-y-8">
<div className="inline-flex items-center space-x-2 bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-semibold">
<span>📊</span>
<span>Free QR Code Tracking</span>
</div>
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
Track Every QR Code Scan with Powerful Analytics
</h1>
<p className="text-xl text-gray-600 leading-relaxed">
Monitor your QR code performance in real-time. Get detailed insights on location, device, time, and user behavior. Make data-driven decisions with our free tracking software.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Start Tracking Free
</Button>
</Link>
<Link href="/create">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Create Trackable QR Code
</Button>
</Link>
</div>
<div className="flex items-center space-x-6 text-sm text-gray-600">
<div className="flex items-center space-x-2">
<svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span>No credit card required</span>
</div>
<div className="flex items-center space-x-2">
<svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span>Unlimited scans</span>
</div>
</div>
</div>
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
Track Every QR Code Scan with Powerful Analytics
</h1>
{/* 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>
<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.
{/* 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 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 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>
</div>
</section>
</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>
{/* 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>
<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>
{/* 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>
{/* 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 ? (
<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-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>
<span className="text-primary-600 font-semibold">{row.qrMaster}</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</Card>
</div>
</div>
</section>
</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,4 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
import { getAuthCookieOptions } from '@/lib/cookieConfig';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
@ -71,16 +74,82 @@ export async function GET(request: NextRequest) {
const userInfo = await userInfoResponse.json();
// Here you would:
// 1. Check if user exists in database
// 2. Create user if they don't exist
// 3. Create session cookie
// 4. Redirect to dashboard
// Check if user exists in database
let user = await db.user.findUnique({
where: { email: userInfo.email },
});
// For now, just redirect to login with error message
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_APP_URL}/login?error=google-not-fully-configured`
);
// Create user if they don't exist
if (!user) {
user = await db.user.create({
data: {
email: userInfo.email,
name: userInfo.name || userInfo.email.split('@')[0],
image: userInfo.picture,
emailVerified: new Date(), // Google already verified the email
password: null, // OAuth users don't need a password
},
});
// Create Account entry for the OAuth provider
await db.account.create({
data: {
userId: user.id,
type: 'oauth',
provider: 'google',
providerAccountId: userInfo.sub || userInfo.id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
token_type: tokens.token_type,
scope: tokens.scope,
id_token: tokens.id_token,
},
});
} else {
// Update existing account tokens
const existingAccount = await db.account.findUnique({
where: {
provider_providerAccountId: {
provider: 'google',
providerAccountId: userInfo.sub || userInfo.id,
},
},
});
if (existingAccount) {
await db.account.update({
where: { id: existingAccount.id },
data: {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
},
});
} else {
// Create Account entry if it doesn't exist
await db.account.create({
data: {
userId: user.id,
type: 'oauth',
provider: 'google',
providerAccountId: userInfo.sub || userInfo.id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_in ? Math.floor(Date.now() / 1000) + tokens.expires_in : null,
token_type: tokens.token_type,
scope: tokens.scope,
id_token: tokens.id_token,
},
});
}
}
// Set authentication cookie
cookies().set('userId', user.id, getAuthCookieOptions());
// Redirect to dashboard
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
} catch (error) {
console.error('Google OAuth error:', error);
return NextResponse.redirect(

View File

@ -1,119 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
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({
title: z.string(),
contentType: z.string(),
content: z.string(),
tags: z.string().optional(),
type: z.enum(['STATIC', 'DYNAMIC']).optional(),
})),
});
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
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);
// Limit bulk creation to 1000 items
if (qrCodes.length > 1000) {
return NextResponse.json(
{ error: 'Maximum 1000 QR codes per bulk upload' },
{ status: 400 }
);
}
// Transform and create QR codes
const createData = qrCodes.map(qr => {
// Parse content based on type
let content: any = { url: qr.content };
if (qr.contentType === 'URL') {
content = { url: qr.content };
} else if (qr.contentType === 'PHONE') {
content = { phone: qr.content };
} else if (qr.contentType === 'EMAIL') {
const [email, subject] = qr.content.split('?subject=');
content = { email, subject };
} else if (qr.contentType === 'TEXT') {
content = { text: qr.content };
} else if (qr.contentType === 'WIFI') {
// Parse format: "NetworkName:password"
const [ssid, password] = qr.content.split(':');
content = { ssid, password, security: 'WPA' };
}
return {
userId: session.user.id!,
title: qr.title,
type: qr.type || 'DYNAMIC',
contentType: qr.contentType as any,
content,
tags: qr.tags ? qr.tags.split(',').map(t => t.trim()) : [],
slug: generateSlug(qr.title),
status: 'ACTIVE' as const,
style: {
foregroundColor: '#000000',
backgroundColor: '#FFFFFF',
cornerStyle: 'square',
size: 200,
},
};
});
// Batch create
const created = await db.qRCode.createMany({
data: createData,
});
return NextResponse.json({
success: true,
count: created.count,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.errors },
{ status: 400 }
);
}
console.error('Bulk upload error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@ -151,18 +151,29 @@ export async function POST(request: NextRequest) {
case 'PHONE':
qrContent = `tel:${body.content.phone}`;
break;
case 'EMAIL':
qrContent = `mailto:${body.content.email}${body.content.subject ? `?subject=${encodeURIComponent(body.content.subject)}` : ''}`;
break;
case 'SMS':
qrContent = `sms:${body.content.phone}${body.content.message ? `?body=${encodeURIComponent(body.content.message)}` : ''}`;
break;
case 'VCARD':
qrContent = `BEGIN:VCARD
VERSION:3.0
FN:${body.content.firstName || ''} ${body.content.lastName || ''}
N:${body.content.lastName || ''};${body.content.firstName || ''};;;
${body.content.organization ? `ORG:${body.content.organization}` : ''}
${body.content.title ? `TITLE:${body.content.title}` : ''}
${body.content.email ? `EMAIL:${body.content.email}` : ''}
${body.content.phone ? `TEL:${body.content.phone}` : ''}
END:VCARD`;
break;
case 'GEO':
const lat = body.content.latitude || 0;
const lon = body.content.longitude || 0;
const label = body.content.label ? `?q=${encodeURIComponent(body.content.label)}` : '';
qrContent = `geo:${lat},${lon}${label}`;
break;
case 'TEXT':
qrContent = body.content.text;
break;
case 'WIFI':
qrContent = `WIFI:T:${body.content.security || 'WPA'};S:${body.content.ssid};P:${body.content.password || ''};H:false;;`;
break;
case 'WHATSAPP':
qrContent = `https://wa.me/${body.content.phone}${body.content.message ? `?text=${encodeURIComponent(body.content.message)}` : ''}`;
break;

View File

@ -23,18 +23,29 @@ export async function POST(request: NextRequest) {
case 'PHONE':
qrContent = `tel:${content.phone}`;
break;
case 'EMAIL':
qrContent = `mailto:${content.email}${content.subject ? `?subject=${encodeURIComponent(content.subject)}` : ''}`;
break;
case 'SMS':
qrContent = `sms:${content.phone}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`;
break;
case 'VCARD':
qrContent = `BEGIN:VCARD
VERSION:3.0
FN:${content.firstName || ''} ${content.lastName || ''}
N:${content.lastName || ''};${content.firstName || ''};;;
${content.organization ? `ORG:${content.organization}` : ''}
${content.title ? `TITLE:${content.title}` : ''}
${content.email ? `EMAIL:${content.email}` : ''}
${content.phone ? `TEL:${content.phone}` : ''}
END:VCARD`;
break;
case 'GEO':
const lat = content.latitude || 0;
const lon = content.longitude || 0;
const label = content.label ? `?q=${encodeURIComponent(content.label)}` : '';
qrContent = `geo:${lat},${lon}${label}`;
break;
case 'TEXT':
qrContent = content.text;
break;
case 'WIFI':
qrContent = `WIFI:T:${content.security || 'WPA'};S:${content.ssid};P:${content.password || ''};H:false;;`;
break;
case 'WHATSAPP':
qrContent = `https://wa.me/${content.phone}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
break;

View File

@ -1,14 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { hashIP } from '@/lib/hash';
import { headers } from 'next/headers';
export async function GET(
request: NextRequest,
{ params }: { params: { slug: string } }
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = params;
const { slug } = await params;
// Fetch QR code by slug
const qrCode = await db.qRCode.findUnique({
@ -43,22 +42,27 @@ export async function GET(
case 'PHONE':
destination = `tel:${content.phone}`;
break;
case 'EMAIL':
destination = `mailto:${content.email}${content.subject ? `?subject=${encodeURIComponent(content.subject)}` : ''}`;
break;
case 'SMS':
destination = `sms:${content.phone}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`;
break;
case 'WHATSAPP':
destination = `https://wa.me/${content.phone}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
break;
case 'VCARD':
// For vCard, redirect to display page
const baseUrlVcard = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
destination = `${baseUrlVcard}/vcard?firstName=${encodeURIComponent(content.firstName || '')}&lastName=${encodeURIComponent(content.lastName || '')}&email=${encodeURIComponent(content.email || '')}&phone=${encodeURIComponent(content.phone || '')}&organization=${encodeURIComponent(content.organization || '')}&title=${encodeURIComponent(content.title || '')}`;
break;
case 'GEO':
// For location, redirect to Google Maps (works on desktop and mobile)
const lat = content.latitude || 0;
const lon = content.longitude || 0;
destination = `https://maps.google.com/?q=${lat},${lon}`;
break;
case 'TEXT':
// For plain text, redirect to a display page
destination = `/display?text=${encodeURIComponent(content.text || '')}`;
break;
case 'WIFI':
// For WiFi, show a connection page
destination = `/wifi?ssid=${encodeURIComponent(content.ssid || '')}&security=${content.security || 'WPA'}`;
const baseUrlText = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
destination = `${baseUrlText}/display?text=${encodeURIComponent(content.text || '')}`;
break;
default:
destination = 'https://example.com';
@ -92,15 +96,14 @@ export async function GET(
async function trackScan(qrId: string, request: NextRequest) {
try {
const headersList = headers();
const userAgent = headersList.get('user-agent') || '';
const referer = headersList.get('referer') || '';
const ip = headersList.get('x-forwarded-for') ||
headersList.get('x-real-ip') ||
const userAgent = request.headers.get('user-agent') || '';
const referer = request.headers.get('referer') || '';
const ip = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown';
// Check DNT header
const dnt = headersList.get('dnt');
const dnt = request.headers.get('dnt');
if (dnt === '1') {
// Respect Do Not Track - only increment counter
await db.qRScan.create({
@ -130,8 +133,8 @@ async function trackScan(qrId: string, request: NextRequest) {
else if (/ios|iphone|ipad/i.test(userAgent)) os = 'iOS';
// Get country from header (Vercel/Cloudflare provide this)
const country = headersList.get('x-vercel-ip-country') ||
headersList.get('cf-ipcountry') ||
const country = request.headers.get('x-vercel-ip-country') ||
request.headers.get('cf-ipcountry') ||
'unknown';
// Extract UTM parameters

274
src/app/vcard/page.tsx Normal file
View File

@ -0,0 +1,274 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
export default function VCardPage() {
const searchParams = useSearchParams();
const [isLoading, setIsLoading] = useState(true);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [organization, setOrganization] = useState('');
const [title, setTitle] = useState('');
const [hasAutoDownloaded, setHasAutoDownloaded] = useState(false);
useEffect(() => {
const firstNameParam = searchParams.get('firstName');
const lastNameParam = searchParams.get('lastName');
const emailParam = searchParams.get('email');
const phoneParam = searchParams.get('phone');
const organizationParam = searchParams.get('organization');
const titleParam = searchParams.get('title');
if (firstNameParam) setFirstName(firstNameParam);
if (lastNameParam) setLastName(lastNameParam);
if (emailParam) setEmail(emailParam);
if (phoneParam) setPhone(phoneParam);
if (organizationParam) setOrganization(organizationParam);
if (titleParam) setTitle(titleParam);
setIsLoading(false);
}, [searchParams]);
// Auto-download after 500ms (only once)
useEffect(() => {
if ((firstName || lastName) && !hasAutoDownloaded) {
const timer = setTimeout(() => {
handleSaveContact();
setHasAutoDownloaded(true);
}, 500);
return () => clearTimeout(timer);
}
}, [firstName, lastName, hasAutoDownloaded]); // eslint-disable-line react-hooks/exhaustive-deps
const handleSaveContact = () => {
// Generate vCard format
const vCard = `BEGIN:VCARD
VERSION:3.0
FN:${firstName} ${lastName}
N:${lastName};${firstName};;;
${organization ? `ORG:${organization}` : ''}
${title ? `TITLE:${title}` : ''}
${email ? `EMAIL:${email}` : ''}
${phone ? `TEL:${phone}` : ''}
END:VCARD`;
// Create a blob and download
const blob = new Blob([vCard], { type: 'text/vcard;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${firstName}_${lastName}.vcf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// Show loading or error state
if (isLoading) {
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
backgroundColor: '#ffffff'
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>👤</div>
<p style={{ color: '#666' }}>Loading contact...</p>
</div>
</div>
);
}
if (!firstName && !lastName) {
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '20px',
backgroundColor: '#ffffff'
}}>
<div style={{
maxWidth: '420px',
width: '100%',
padding: '48px 32px',
textAlign: 'center'
}}>
<div style={{
fontSize: '64px',
marginBottom: '24px',
opacity: 0.3
}}>👤</div>
<h1 style={{
fontSize: '22px',
fontWeight: '600',
marginBottom: '12px',
color: '#1a1a1a'
}}>No Contact Found</h1>
<p style={{
color: '#666',
fontSize: '15px',
lineHeight: '1.6'
}}>This QR code doesn't contain any contact information.</p>
</div>
</div>
);
}
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '20px',
backgroundColor: '#ffffff'
}}>
<div style={{
maxWidth: '420px',
width: '100%',
padding: '48px 32px'
}}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{
width: '80px',
height: '80px',
margin: '0 auto 20px',
borderRadius: '50%',
backgroundColor: '#f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '36px'
}}>👤</div>
<h1 style={{
fontSize: '28px',
fontWeight: '600',
marginBottom: '8px',
color: '#1a1a1a',
letterSpacing: '-0.02em'
}}>
{firstName} {lastName}
</h1>
{(title || organization) && (
<p style={{
color: '#666',
fontSize: '16px',
marginTop: '4px'
}}>
{title && organization && `${title} at ${organization}`}
{title && !organization && title}
{!title && organization && organization}
</p>
)}
</div>
{/* Contact Details */}
<div style={{ marginBottom: '32px' }}>
{email && (
<div style={{
padding: '16px 20px',
backgroundColor: '#f8f8f8',
borderRadius: '12px',
marginBottom: '12px',
border: '1px solid #f0f0f0'
}}>
<div style={{
fontSize: '11px',
color: '#888',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontWeight: '500'
}}>Email</div>
<a href={`mailto:${email}`} style={{
fontSize: '15px',
fontWeight: '500',
color: '#2563eb',
textDecoration: 'none',
wordBreak: 'break-all'
}}>{email}</a>
</div>
)}
{phone && (
<div style={{
padding: '16px 20px',
backgroundColor: '#f8f8f8',
borderRadius: '12px',
border: '1px solid #f0f0f0'
}}>
<div style={{
fontSize: '11px',
color: '#888',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontWeight: '500'
}}>Phone</div>
<a href={`tel:${phone}`} style={{
fontSize: '15px',
fontWeight: '500',
color: '#2563eb',
textDecoration: 'none'
}}>{phone}</a>
</div>
)}
</div>
{/* Save Button */}
<button
onClick={handleSaveContact}
style={{
width: '100%',
padding: '16px',
fontSize: '16px',
fontWeight: '600',
color: 'white',
backgroundColor: '#2563eb',
border: 'none',
borderRadius: '12px',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 2px 8px rgba(37, 99, 235, 0.2)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#1d4ed8';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#2563eb';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(37, 99, 235, 0.2)';
}}
>
Save to Contacts
</button>
<p style={{
textAlign: 'center',
fontSize: '13px',
color: '#999',
marginTop: '16px',
lineHeight: '1.5'
}}>
Add this contact to your address book
</p>
</div>
</div>
);
}

View File

@ -45,8 +45,22 @@ export const QRCodeCard: React.FC<QRCodeCardProps> = ({
qrUrl = qr.content.url;
} else if (qr.contentType === 'PHONE' && qr.content?.phone) {
qrUrl = `tel:${qr.content.phone}`;
} else if (qr.contentType === 'EMAIL' && qr.content?.email) {
qrUrl = `mailto:${qr.content.email}`;
} else if (qr.contentType === 'VCARD') {
// VCARD content needs to be formatted properly
qrUrl = `BEGIN:VCARD
VERSION:3.0
FN:${qr.content.firstName || ''} ${qr.content.lastName || ''}
N:${qr.content.lastName || ''};${qr.content.firstName || ''};;;
${qr.content.organization ? `ORG:${qr.content.organization}` : ''}
${qr.content.title ? `TITLE:${qr.content.title}` : ''}
${qr.content.email ? `EMAIL:${qr.content.email}` : ''}
${qr.content.phone ? `TEL:${qr.content.phone}` : ''}
END:VCARD`;
} else if (qr.contentType === 'GEO' && qr.content) {
const lat = qr.content.latitude || 0;
const lon = qr.content.longitude || 0;
const label = qr.content.label ? `?q=${encodeURIComponent(qr.content.label)}` : '';
qrUrl = `geo:${lat},${lon}${label}`;
} else if (qr.contentType === 'TEXT' && qr.content?.text) {
qrUrl = qr.content.text;
} else if (qr.content?.qrContent) {

View File

@ -13,13 +13,13 @@ interface HeroProps {
export const Hero: React.FC<HeroProps> = ({ t }) => {
const templateCards = [
{ title: 'URL/Website', color: 'bg-blue-100', icon: '🌐' },
{ title: 'WiFi', color: 'bg-purple-100', icon: '📶' },
{ title: 'Email', color: 'bg-green-100', icon: '📧' },
{ title: 'Contact Card', color: 'bg-purple-100', icon: '👤' },
{ title: 'Location', color: 'bg-green-100', icon: '📍' },
{ title: 'Phone Number', color: 'bg-pink-100', icon: '📞' },
];
return (
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 py-20">
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-purple-50 pt-12 pb-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">
{/* Left Content */}

View File

@ -1,6 +1,7 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
@ -77,23 +78,19 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
))}
</ul>
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
<Link href="/signup">
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
</Link>
</CardContent>
</Card>
))}
</div>
<div className="text-center mt-8">
<a href="/#pricing" className="text-primary-600 hover:text-primary-700 font-medium">
View Full Pricing Details
</a>
</div>
</div>
</section>
);

View File

@ -25,9 +25,9 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
iconBg: 'bg-blue-100',
},
{
key: 'wifi',
title: t.templates.wifi,
icon: '📶',
key: 'vcard',
title: t.templates.vcard,
icon: '👤',
color: 'bg-purple-50 border-purple-200',
iconBg: 'bg-purple-100',
},

View File

@ -7,7 +7,17 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, label, error, ...props }, ref) => {
({ className, type, label, error, onInvalid, ...props }, ref) => {
// Default English validation message
const handleInvalid = (e: React.InvalidEvent<HTMLInputElement>) => {
e.target.setCustomValidity('Please fill out this field.');
if (onInvalid) onInvalid(e);
};
const handleInput = (e: React.FormEvent<HTMLInputElement>) => {
e.currentTarget.setCustomValidity('');
};
return (
<div className="space-y-1">
{label && (
@ -23,6 +33,8 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
className
)}
ref={ref}
onInvalid={handleInvalid}
onInput={handleInput}
{...props}
/>
{error && (

View File

@ -45,7 +45,7 @@
"title": "Mit einer Vorlage beginnen",
"restaurant": "Restaurant-Menü",
"business": "Visitenkarte",
"wifi": "WLAN-Zugang",
"vcard": "Kontaktkarte",
"event": "Event-Ticket",
"use_template": "Vorlage verwenden →"
},

View File

@ -43,7 +43,7 @@
"title": "Start with a Template",
"restaurant": "Restaurant Menu",
"business": "Business Card",
"wifi": "Wi-Fi Access",
"vcard": "Contact Card",
"event": "Event Ticket",
"use_template": "Use template →"
},

View File

@ -22,15 +22,17 @@ const qrContentSchema = z.object({
url: z.string().url().optional(),
phone: z.string().optional(),
email: z.string().email().optional(),
subject: z.string().optional(),
message: z.string().optional(),
text: z.string().optional(),
ssid: z.string().optional(),
password: z.string().optional(),
security: z.enum(['WPA', 'WEP', 'nopass']).optional(),
// VCARD fields
firstName: z.string().optional(),
lastName: z.string().optional(),
organization: z.string().optional(),
title: z.string().optional(),
// GEO fields
latitude: z.number().optional(),
longitude: z.number().optional(),
label: z.string().optional(),
});
const qrStyleSchema = z.object({
@ -128,25 +130,26 @@ export function getQRContent(qr: any): string {
return content.url || '';
case 'PHONE':
return `tel:${content.phone || ''}`;
case 'EMAIL':
const subject = content.subject ? `?subject=${encodeURIComponent(content.subject)}` : '';
return `mailto:${content.email || ''}${subject}`;
case 'SMS':
const message = content.message ? `?body=${encodeURIComponent(content.message)}` : '';
return `sms:${content.phone || ''}${message}`;
case 'WHATSAPP':
const whatsappMessage = content.message ? `?text=${encodeURIComponent(content.message)}` : '';
return `https://wa.me/${content.phone || ''}${whatsappMessage}`;
case 'WIFI':
return `WIFI:T:${content.security || 'WPA'};S:${content.ssid || ''};P:${content.password || ''};;`;
case 'VCARD':
return `BEGIN:VCARD
VERSION:3.0
FN:${content.firstName || ''} ${content.lastName || ''}
ORG:${content.organization || ''}
TITLE:${content.title || ''}
EMAIL:${content.email || ''}
TEL:${content.phone || ''}
END:VCARD`;
case 'GEO':
const lat = content.latitude || 0;
const lon = content.longitude || 0;
const label = content.label ? `?q=${encodeURIComponent(content.label)}` : '';
return `geo:${lat},${lon}${label}`;
case 'TEXT':
return content.text || '';
default:

View File

@ -35,10 +35,10 @@ export const STRIPE_PLANS = {
currency: 'EUR',
interval: 'month',
features: [
'50 QR-Codes',
'Branding (Farben)',
'Detaillierte Analytics (Datum, Gerät, Stadt)',
'CSV-Export',
'50 QR Codes',
'Branding (Colors)',
'Detailed Analytics (Date, Device, City)',
'CSV Export',
'SVG/PNG Download',
],
limits: {
@ -57,9 +57,9 @@ export const STRIPE_PLANS = {
interval: 'month',
features: [
'500 QR-Codes',
'Alles von Pro',
'Bulk QR-Generierung (bis 1,000)',
'Prioritäts-Support',
'Everything from Pro',
'Bulk QR Generation (up to 1,000)',
'Priority Support',
],
limits: {
dynamicQRCodes: 500,

View File

@ -25,7 +25,7 @@ export const createQRSchema = z.object({
isStatic: z.boolean().optional(),
contentType: z.enum(['URL', 'WIFI', 'EMAIL', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT'], {
contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT'], {
errorMap: () => ({ message: 'Invalid content type' })
}),
@ -60,7 +60,7 @@ export const bulkQRSchema = z.object({
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']),
contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT']),
})
).min(1, 'At least one QR code is required')
.max(100, 'Maximum 100 QR codes per bulk creation'),