From f31992b9529f16ba424863f98865727149134c02 Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Wed, 5 Nov 2025 12:02:59 +0100 Subject: [PATCH] =?UTF-8?q?Wichige=20=C3=A4nderung=20an=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION_FROM_SUPABASE.md | 6 + README.md | 242 ++- env.example | 22 +- next.config.mjs | 7 +- package-lock.json | 1474 +++++++---------- package.json | 1 + prisma/schema.prisma | 6 +- prisma/seed.ts | 17 +- src/app/(app)/analytics/page.tsx | 44 +- src/app/(app)/bulk-creation/page.tsx | 145 +- src/app/(app)/bulk/page.tsx | 410 ----- src/app/(app)/create/page.tsx | 100 +- src/app/(app)/dashboard/page.tsx | 8 +- src/app/(app)/qr/[id]/edit/page.tsx | 66 +- src/app/(auth)/login/page.tsx | 18 - src/app/(marketing)/blog/[slug]/page.tsx | 207 +-- src/app/(marketing)/blog/page.tsx | 11 +- .../bulk-qr-code-generator/page.tsx | 772 ++++++--- .../dynamic-qr-code-generator/page.tsx | 619 ++++--- src/app/(marketing)/layout.tsx | 16 +- src/app/(marketing)/qr-code-tracking/page.tsx | 500 +++--- src/app/api/auth/google/route.ts | 87 +- src/app/api/bulk/route.ts | 119 -- src/app/api/qrs/route.ts | 23 +- src/app/api/qrs/static/route.ts | 23 +- src/app/r/[slug]/route.ts | 43 +- src/app/vcard/page.tsx | 274 +++ src/components/dashboard/QRCodeCard.tsx | 18 +- src/components/marketing/Hero.tsx | 6 +- src/components/marketing/Pricing.tsx | 23 +- src/components/marketing/TemplateCards.tsx | 6 +- src/components/ui/Input.tsx | 14 +- src/i18n/de.json | 2 +- src/i18n/en.json | 2 +- src/lib/qr.ts | 21 +- src/lib/stripe.ts | 14 +- src/lib/validationSchemas.ts | 4 +- 37 files changed, 2774 insertions(+), 2596 deletions(-) delete mode 100644 src/app/(app)/bulk/page.tsx delete mode 100644 src/app/api/bulk/route.ts create mode 100644 src/app/vcard/page.tsx diff --git a/MIGRATION_FROM_SUPABASE.md b/MIGRATION_FROM_SUPABASE.md index 8eec218..ab8d15c 100644 --- a/MIGRATION_FROM_SUPABASE.md +++ b/MIGRATION_FROM_SUPABASE.md @@ -313,3 +313,9 @@ For issues: πŸŽ‰ **Congratulations!** You've successfully migrated from Supabase to local PostgreSQL! + + + + + + diff --git a/README.md b/README.md index 2693a7c..fda3558 100644 --- a/README.md +++ b/README.md @@ -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 /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 diff --git a/env.example b/env.example index 15e8222..db3ca84 100644 --- a/env.example +++ b/env.example @@ -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 diff --git a/next.config.mjs b/next.config.mjs index 5bd78cc..6a8ea98 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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'] diff --git a/package-lock.json b/package-lock.json index 1ea01f2..f5b3180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,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", @@ -144,9 +145,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", "dev": true, "license": "MIT", "optional": true, @@ -156,9 +157,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", "license": "MIT", "optional": true, "dependencies": { @@ -177,9 +178,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -194,9 +195,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -211,9 +212,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -228,9 +229,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -245,9 +246,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -262,9 +263,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -279,9 +280,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -296,9 +297,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -313,9 +314,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -330,9 +331,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -347,9 +348,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -364,9 +365,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -381,9 +382,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -398,9 +399,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -415,9 +416,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -432,9 +433,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -449,9 +450,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -466,9 +467,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -483,9 +484,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -500,9 +501,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -517,9 +518,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -534,9 +535,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -551,9 +552,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -568,9 +569,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -585,9 +586,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -602,9 +603,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -638,9 +639,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -1117,37 +1118,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -1164,24 +1134,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1256,74 +1208,6 @@ "glob": "10.3.10" } }, - "node_modules/@next/eslint-plugin-next/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@next/swc-darwin-arm64": { "version": "14.2.18", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", @@ -1537,9 +1421,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz", - "integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.5.0.tgz", + "integrity": "sha512-oxfV20QMNwH30jKybUyqi3yGuMghULQz1zkJgQG3rjpHDxhD2vDN6E7UpmaqgphMIvGG3Q+DgfU10zfSPA7w7w==", "license": "MIT" }, "node_modules/@prisma/client": { @@ -1618,16 +1502,16 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz", - "integrity": "sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.1.tgz", + "integrity": "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ==", "dev": true, "license": "MIT" }, "node_modules/@stripe/stripe-js": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.0.0.tgz", - "integrity": "sha512-dLvD55KT1cBmrqzgYRgY42qNcw6zW4HS5oRZs0xRvHw9gBWig5yDnWNop/E+/t2JK+OZO30zsnupVBN2MqW2mg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.3.0.tgz", + "integrity": "sha512-hmC3u0anySPI+As+wFGrKG38pXnXAUqyiitnYeYknEOslcDM96AkBdMNxKYOopecuP/v1HMbclUEq8/DKKn+6w==", "license": "MIT", "engines": { "node": ">=12.16" @@ -1688,9 +1572,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", - "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", "dependencies": { @@ -1698,9 +1582,9 @@ } }, "node_modules/@types/papaparse": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz", - "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.0.tgz", + "integrity": "sha512-GVs5iMQmUr54BAZYYkByv8zPofFxmyxUpISPb2oh8sayR3+1zbxasrOvoKiHJ/nnoq/uULuPsu1Lze1EkagVFg==", "dev": true, "license": "MIT", "dependencies": { @@ -1715,9 +1599,9 @@ "license": "MIT" }, "node_modules/@types/qrcode": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", - "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", "dev": true, "license": "MIT", "dependencies": { @@ -1746,17 +1630,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", - "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/type-utils": "8.46.1", - "@typescript-eslint/utils": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1770,74 +1654,11 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.1", + "@typescript-eslint/parser": "^8.46.3", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -1848,57 +1669,40 @@ "node": ">= 4" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", - "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.1", - "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", "debug": "^4.3.4" }, "engines": { @@ -1912,12 +1716,16 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1926,28 +1734,10 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", - "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", "dev": true, "license": "MIT", "engines": { @@ -1962,15 +1752,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", - "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1986,10 +1776,10 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", "dev": true, "license": "MIT", "engines": { @@ -2000,17 +1790,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.1", - "@typescript-eslint/tsconfig-utils": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2029,119 +1819,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2153,148 +1830,6 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", - "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.46.1", - "@typescript-eslint/tsconfig-utils": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", @@ -2310,35 +1845,59 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@ungap/structured-clone": { @@ -2775,16 +2334,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -3013,9 +2562,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", - "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "version": "2.8.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.24.tgz", + "integrity": "sha512-uUhTRDPXamakPyghwrUcjaGvvBqGrWvBHReoiULMIpOJVM9IYzQh83Xk2Onx5HlGI2o10NNCzcs9TG/S3TkwrQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3066,9 +2615,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "dev": true, "funding": [ { @@ -3086,11 +2635,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -3188,9 +2737,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001750", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", "funding": [ { "type": "opencollective", @@ -3304,6 +2853,40 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3524,9 +3107,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debug": { @@ -3629,19 +3212,6 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -3684,9 +3254,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.234", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", - "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "version": "1.5.245", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz", + "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==", "dev": true, "license": "ISC" }, @@ -3872,9 +3442,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3885,32 +3455,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -4745,9 +4315,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4758,22 +4328,23 @@ } }, "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4792,6 +4363,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -4825,27 +4422,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5072,9 +4648,9 @@ } }, "node_modules/ioredis": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.1.tgz", - "integrity": "sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", "dependencies": { "@ioredis/commands": "1.4.0", @@ -5536,10 +5112,9 @@ } }, "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/isexe": { @@ -5568,14 +5143,17 @@ } }, "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": ">=14" + }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -5991,9 +5569,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.11", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", - "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", @@ -6007,9 +5585,9 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "@auth/core": "0.34.2", - "next": "^12.2.5 || ^13 || ^14 || ^15", - "nodemailer": "^6.6.5", + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, @@ -6113,9 +5691,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -6285,9 +5863,9 @@ } }, "node_modules/oidc-token-hash": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz", - "integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", "license": "MIT", "engines": { "node": "^10.13.0 || >=12.0.0" @@ -6404,13 +5982,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -6496,16 +6067,6 @@ "dev": true, "license": "ISC" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6728,28 +6289,16 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.276.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.276.0.tgz", - "integrity": "sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw==", + "version": "1.285.1", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.285.1.tgz", + "integrity": "sha512-+Cn3igI6LoOJuGtsiVsD15a0jGIaR1zE5tEG3Ovd4MLgti3RFHdS1eOisZTJUuQa2bR8X/Bx7HdoXsGNmW9h4Q==", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@posthog/core": "1.3.0", + "@posthog/core": "1.5.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" - }, - "peerDependencies": { - "@rrweb/types": "2.0.0-alpha.17", - "rrweb-snapshot": "2.0.0-alpha.17" - }, - "peerDependenciesMeta": { - "@rrweb/types": { - "optional": true - }, - "rrweb-snapshot": { - "optional": true - } } }, "node_modules/posthog-js/node_modules/preact": { @@ -6863,6 +6412,18 @@ "node": ">=6" } }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "license": "MIT", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -6880,6 +6441,12 @@ "node": ">=10.13.0" } }, + "node_modules/qrcode-generator": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", + "license": "MIT" + }, "node_modules/qrcode.react": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.2.0.tgz", @@ -6938,9 +6505,9 @@ } }, "node_modules/react-chartjs-2": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", - "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", "license": "MIT", "peerDependencies": { "chart.js": "^4.1.1", @@ -7030,12 +6597,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7130,13 +6691,13 @@ "license": "ISC" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -7198,6 +6759,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7242,6 +6825,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -7265,6 +6855,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -7521,16 +7118,6 @@ "is-arrayish": "^0.3.1" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7597,17 +7184,21 @@ } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -7633,11 +7224,34 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, "node_modules/string.prototype.includes": { "version": "2.0.1", @@ -7802,9 +7416,9 @@ } }, "node_modules/stripe": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.1.0.tgz", - "integrity": "sha512-FjgIiE98dMMTNssfdjMvFdD4eZyEzdWAOwPYqzhPRNZeg9ggFWlPXmX1iJKD5pPIwZBaPlC3SayQQkwsPo6/YQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz", + "integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==", "license": "MIT", "dependencies": { "qs": "^6.11.0" @@ -7867,53 +7481,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8072,16 +7639,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -8310,9 +7877,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -8448,6 +8015,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", @@ -8524,17 +8098,21 @@ } }, "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { @@ -8556,6 +8134,70 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8631,6 +8273,12 @@ "node": ">=6" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/yargs/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -8683,6 +8331,20 @@ "node": ">=8" } }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 525db9f..442cce2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 77e3ef3..cd41799 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/prisma/seed.ts b/prisma/seed.ts index 7712f27..08d2a7f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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', diff --git a/src/app/(app)/analytics/page.tsx b/src/app/(app)/analytics/page.tsx index f8a4e23..41b4f88 100644 --- a/src/app/(app)/analytics/page.tsx +++ b/src/app/(app)/analytics/page.tsx @@ -331,19 +331,19 @@ export default function AnalyticsPage() { - - - - + + + + {analyticsData.countryStats.map((country: any, index: number) => ( - - - - - + + + + @@ -366,27 +366,27 @@ export default function AnalyticsPage() {
CountryScansPercentageTrendCountryScansPercentageTrend
{country.country}{country.count.toLocaleString()}{country.percentage}% +
{country.country}{country.count.toLocaleString()}{country.percentage}% ↑
- - - - - - + + + + + + {analyticsData.qrPerformance.map((qr: any) => ( - - - + + - - - - + + + + {fileFormat.map((field, index) => ( + + + + + + ))} + +
QR CodeTypeTotal ScansUnique ScansConversionTrendQR CodeTypeTotal ScansUnique ScansConversionTrend
{qr.title} +
{qr.title} {qr.type} {qr.totalScans.toLocaleString()}{qr.uniqueScans.toLocaleString()}{qr.conversion}% + {qr.totalScans.toLocaleString()}{qr.uniqueScans.toLocaleString()}{qr.conversion}% 0 ? 'success' : 'default'}> {qr.totalScans > 0 ? '↑' : 'β€”'} diff --git a/src/app/(app)/bulk-creation/page.tsx b/src/app/(app)/bulk-creation/page.tsx index 8d08211..fc1d7c5 100644 --- a/src/app/(app)/bulk-creation/page.tsx +++ b/src/app/(app)/bulk-creation/page.tsx @@ -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() {
-
+
1
Upload File
-
+
-
-
+
2
Preview & Map
-
+
-
+
3
Download @@ -378,9 +380,8 @@ export default function BulkCreationPage() {
@@ -442,6 +443,110 @@ export default function BulkCreationPage() {
+ + {/* Supported QR Code Types Section */} +
+ + + πŸ“‹ Supported QR Code Types + + +
+

+ This bulk generator creates static QR codes for multiple content types. Choose the format that matches your needs: +

+ +
+
+

🌐 URL - Website Links

+

Format: https://example.com

+

Example: Product Page,https://example.com/product

+
+ +
+

πŸ‘€ VCARD - Contact Cards

+

Format: FirstName,LastName,Email,Phone,Organization,Title

+

Example: John Doe,"John,Doe,john@example.com,+1234567890,Company,CEO"

+
+ +
+

πŸ“ GEO - Locations

+

Format: latitude,longitude,label

+

Example: Office Location,"37.7749,-122.4194,Main Office"

+
+ +
+

πŸ“ž PHONE - Phone Numbers

+

Format: +1234567890

+

Example: Support Hotline,+1234567890

+
+ +
+

πŸ“ TEXT - Plain Text

+

Format: Any text content

+

Example: Serial Number,SN-12345-ABCDE

+
+
+ +
+

πŸ“₯ CSV File Format:

+

+ Your file needs two columns: title and content +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
titlecontent
Product Pagehttps://example.com/product
John DoeJohn,Doe,john@example.com,+1234567890,Company,CEO
Office Location37.7749,-122.4194,Main Office
Support Hotline+1234567890
Serial NumberSN-12345-ABCDE
+
+
+ +
+

ℹ️ Important Notes

+
    +
  • β€’ Static QR codes - Cannot be edited after creation
  • +
  • β€’ No tracking or analytics - Scans are not tracked
  • +
  • β€’ Maximum 1,000 QR codes per upload
  • +
  • β€’ Download as ZIP or save to your dashboard
  • +
  • β€’ All QR types supported - URLs, vCards, locations, phone numbers, and text
  • +
+
+ +
+

+ πŸ’‘ Tip: Download the template above to see examples of all 5 QR code types with 11 ready-to-use examples! +

+
+
+
+
+
)} diff --git a/src/app/(app)/bulk/page.tsx b/src/app/(app)/bulk/page.tsx deleted file mode 100644 index f40d5f9..0000000 --- a/src/app/(app)/bulk/page.tsx +++ /dev/null @@ -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([]); - const [mapping, setMapping] = useState>({}); - const [loading, setLoading] = useState(false); - const [uploadResult, setUploadResult] = useState(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 = {}; - - 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 ( -
-
-

{t('bulk.title')}

-

{t('bulk.subtitle')}

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

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

-

or click to browse

-

Supports CSV, XLS, XLSX (max 1000 rows)

-
- -
- - -
-
- - - -
-
-

CSV Format

-

Comma-separated values

-
-
-
-
- - - -
-
- - - -
-
-

Excel Format

-

XLS or XLSX files

-
-
-
-
- - - -
-
- - - -
-
-

Fast Processing

-

Up to 1000 QR codes

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

- Showing 5 of {data.length} rows -

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

Upload Complete!

-

- Successfully created {data.length} QR codes -

-
- - -
-
-
- )} -
- ); -} \ No newline at end of file diff --git a/src/app/(app)/create/page.tsx b/src/app/(app)/create/page.tsx index 162db88..e0a0f73 100644 --- a/src/app/(app)/create/page.tsx +++ b/src/app/(app)/create/page.tsx @@ -26,7 +26,6 @@ export default function CreatePage() { const [contentType, setContentType] = useState('URL'); const [content, setContent] = useState({ 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 ( <> + setContent({ ...content, firstName: e.target.value })} + placeholder="John" + required + /> + setContent({ ...content, lastName: e.target.value })} + placeholder="Doe" + required + /> setContent({ ...content, email: e.target.value })} - placeholder="contact@example.com" - required + placeholder="john@example.com" /> 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" + /> + setContent({ ...content, organization: e.target.value })} + placeholder="Company Name" + /> + setContent({ ...content, title: e.target.value })} + placeholder="CEO" /> ); - case 'WIFI': + case 'GEO': return ( <> 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 /> 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 /> - setContent({ ...content, label: e.target.value })} + placeholder="Golden Gate Bridge" /> ); @@ -318,13 +347,6 @@ export default function CreatePage() { /> {renderContentFields()} - - setTags(e.target.value)} - placeholder="marketing, campaign, 2025" - /> diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 16c3e68..a3d346e 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -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', }); diff --git a/src/app/(app)/qr/[id]/edit/page.tsx b/src/app/(app)/qr/[id]/edit/page.tsx index abdbb05..8c99278 100644 --- a/src/app/(app)/qr/[id]/edit/page.tsx +++ b/src/app/(app)/qr/[id]/edit/page.tsx @@ -154,21 +154,75 @@ export default function EditQRPage() { /> )} - {qrCode.contentType === 'EMAIL' && ( + {qrCode.contentType === 'VCARD' && ( <> + setContent({ ...content, firstName: e.target.value })} + placeholder="John" + required + /> + setContent({ ...content, lastName: e.target.value })} + placeholder="Doe" + required + /> setContent({ ...content, email: e.target.value })} - placeholder="email@example.com" + placeholder="john@example.com" + /> + setContent({ ...content, phone: e.target.value })} + placeholder="+1234567890" + /> + setContent({ ...content, organization: e.target.value })} + placeholder="Company Name" + /> + setContent({ ...content, title: e.target.value })} + placeholder="CEO" + /> + + )} + + {qrCode.contentType === 'GEO' && ( + <> + setContent({ ...content, latitude: parseFloat(e.target.value) || 0 })} + placeholder="37.7749" required /> 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 + /> + setContent({ ...content, label: e.target.value })} + placeholder="Golden Gate Bridge" /> )} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 68e11f6..155cbbc 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -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 (
@@ -149,18 +143,6 @@ export default function LoginPage() { Sign in with Google - -
diff --git a/src/app/(marketing)/blog/[slug]/page.tsx b/src/app/(marketing)/blog/[slug]/page.tsx index 6595dad..7e121ad 100644 --- a/src/app/(marketing)/blog/[slug]/page.tsx +++ b/src/app/(marketing)/blog/[slug]/page.tsx @@ -144,193 +144,6 @@ const blogPosts: Record = {

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.

`, }, - '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: `
-

Was sind statische QR-Codes?

-

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.

- -

Vorteile statischer QR-Codes

-

FΓΌr immer gΓΌltig: 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.

-

Keine laufenden Kosten: Da keine Server-Infrastruktur erforderlich ist, fallen keine monatlichen GebΓΌhren an.

-

Schneller Scan: Direkter Zugriff auf Inhalte ohne Umleitung ΓΌber Server.

- -

Nachteile statischer QR-Codes

-

Nicht editierbar: Nach dem Druck kΓΆnnen Sie die verlinkten Inhalte nicht mehr Γ€ndern.

-

Keine Analytics: Sie kΓΆnnen nicht nachverfolgen, wie oft der Code gescannt wurde.

-

Grâßer bei langen URLs: Je mehr Daten eingebettet sind, desto komplexer und grâßer wird der QR-Code.

- -

Was sind dynamische QR-Codes?

-

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.

- -

Vorteile dynamischer QR-Codes

-

Editierbar: Γ„ndern Sie die Ziel-URL jederzeit, auch nach dem Druck.

-

Detaillierte Analytics: Verfolgen Sie Scans nach Standort, GerΓ€t, Zeit und mehr.

-

Retargeting-fΓ€hig: Nutzen Sie Scan-Daten fΓΌr personalisierte Marketing-Kampagnen.

-

Kompakter Code: Da nur eine kurze Umleitungs-URL eingebettet ist, bleibt der QR-Code kleiner und einfacher zu scannen.

- -

Nachteile dynamischer QR-Codes

-

Erfordert Abo: Dynamische QR-Codes benΓΆtigen eine aktive Server-Infrastruktur, was meist mit monatlichen Kosten verbunden ist.

-

AbhΓ€ngig vom Server: Wenn der Server ausfΓ€llt oder das Abo ablΓ€uft, funktioniert der QR-Code nicht mehr.

- -

Vergleichstabelle: Statisch vs. Dynamisch

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureStatischDynamisch
EditierbarNeinJa
AnalyticsNeinJa
KostenKostenlosAbo erforderlich
GrâßeGrâßer bei langen URLsImmer kompakt
GΓΌltigkeitsdauerFΓΌr immerAbhΓ€ngig vom Abo
-
- -

Wann sollten Sie welchen QR-Code-Typ verwenden?

-

Verwenden Sie statische QR-Codes fΓΌr:

-

β€’ Visitenkarten mit festen Kontaktdaten

-

β€’ WLAN-PasswΓΆrter

-

β€’ Produktverpackungen mit permanenten URLs

-

β€’ Anwendungen, bei denen Sie keine Analytics benΓΆtigen

- -

Verwenden Sie dynamische QR-Codes fΓΌr:

-

β€’ Marketing-Kampagnen mit wechselnden Angeboten

-

β€’ Event-Tickets mit aktualisierbaren Informationen

-

β€’ Print-Anzeigen, bei denen Sie die Landingpage optimieren mΓΆchten

-

β€’ Jede Anwendung, bei der Sie Scan-Statistiken tracken mΓΆchten

- -

Fazit

-

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?

-
`, - }, - '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: `
-

Warum QR-Codes fΓΌr Restaurants?

-

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.

- -

Digitale Speisekarten mit QR-Codes

-

Vorteile fΓΌr Restaurants

-

Kosten sparen: Keine teuren Speisekarten-Neudrucke bei PreisΓ€nderungen oder neuen Gerichten.

-

Aktualisierungen in Echtzeit: Passen Sie Ihr MenΓΌ sofort an – tΓ€glich wechselnde Angebote, VerfΓΌgbarkeiten oder Allergiehinweise.

-

Mehrsprachigkeit: Bieten Sie Ihre Speisekarte automatisch in mehreren Sprachen an.

-

Hygiene: GΓ€ste scannen mit ihrem eigenen Smartphone – keine gemeinsam genutzten Speisekarten mehr.

- -

Vorteile fΓΌr GΓ€ste

-

β€’ Sofortiger Zugriff auf die Speisekarte ohne Warten

-

β€’ HochauflΓΆsende Bilder der Gerichte

-

β€’ Detaillierte NΓ€hrwertangaben und Allergiehinweise

-

β€’ Personalisierte Empfehlungen

- -

Kontaktloses Bestellen via QR-Code

-

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.

- -

Vorteile des kontaktlosen Bestellens

-

Schnellerer Service: Bestellungen gehen direkt in die KΓΌche, ohne Zwischenschritte.

-

Weniger Fehler: GΓ€ste geben ihre Bestellung selbst ein – keine MissverstΓ€ndnisse mehr.

-

Upselling-MΓΆglichkeiten: Zeigen Sie automatisch passende Empfehlungen oder Zusatzprodukte an.

-

HΓΆhere Tischrotation: Weniger Wartezeiten bedeuten mehr bediente GΓ€ste pro Abend.

- -

QR-Code-Platzierung im Restaurant

-

Auf dem Tisch

-

Der klassische Ansatz: QR-Codes auf Tischaufstellern, Tischsets oder direkt auf dem Tisch. Ideal fΓΌr Restaurants mit festem Sitzplatz.

- -

An der Wand oder am Eingang

-

FΓΌr Schnellrestaurants oder CafΓ©s: GΓ€ste scannen beim Betreten und bestellen an der Theke oder am Tisch.

- -

Auf Rechnungen und Flyern

-

Nutzen Sie QR-Codes auf Rechnungen fΓΌr Online-Bewertungen, Treueprogramme oder Social-Media-Verlinkungen.

- -

Best Practices fΓΌr Restaurant-QR-Codes

-

1. Design: Markenkonform und ansprechend

-

Gestalten Sie QR-Codes in Ihren Markenfarben mit Ihrem Logo. Ein professionelles Design erhΓΆht die Scan-Rate.

- -

2. Klare Anweisungen

-

FΓΌgen Sie einen Call-to-Action hinzu: "Scannen fΓΌr Speisekarte" oder "Hier bestellen". Nicht jeder Gast ist mit QR-Codes vertraut.

- -

3. Mobile Optimierung

-

Ihre digitale Speisekarte MUSS mobilfreundlich sein. Große Schriftarten, einfache Navigation, schnelle Ladezeiten.

- -

4. Testen Sie regelmÀßig

-

PrΓΌfen Sie wΓΆchentlich, ob alle QR-Codes funktionieren und zur richtigen Seite fΓΌhren.

- -

Analytics: Messen Sie den Erfolg

-

Mit dynamischen QR-Codes erhalten Sie wertvolle Insights:

-

β€’ Wie viele GΓ€ste scannen den QR-Code?

-

β€’ Welche Gerichte werden am hΓ€ufigsten angesehen?

-

β€’ Zu welchen Uhrzeiten ist die Nachfrage am hΓΆchsten?

-

β€’ Welche Tische haben die hΓΆchste Scan-Rate?

- -

Praxisbeispiel: Pizza-Restaurant "Bella Italia"

-

Das fiktive Restaurant "Bella Italia" fΓΌhrte QR-Code-Bestellungen ein und erzielte innerhalb von 3 Monaten:

-

β€’ 40% schnellere Bestellabwicklung

-

β€’ 25% hΓΆherer Umsatz durch Upselling-VorschlΓ€ge

-

β€’ 90% der GΓ€ste bevorzugen QR-Bestellungen gegenΓΌber klassischer Bedienung

-

β€’ 50% Kosteneinsparung bei Speisekarten-Druck

- -

HΓ€ufige Fragen (FAQ)

-

Was, wenn GΓ€ste kein Smartphone haben?

-

Halten Sie einige gedruckte Speisekarten als Backup bereit – besonders fΓΌr Γ€ltere GΓ€ste.

- -

Wie sicher ist das Bezahlen via QR-Code?

-

Nutzen Sie etablierte Payment-Anbieter wie Stripe oder PayPal, die hΓΆchste Sicherheitsstandards erfΓΌllen.

- -

Brauche ich technisches Know-how?

-

Nein! Plattformen wie QR Master ermΓΆglichen die Erstellung und Verwaltung von Restaurant-QR-Codes ohne Programmierkenntnisse.

- -

Fazit

-

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!

-
`, - }, '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: `

Choosing between static and dynamic QR codes is one of the most important decisions when implementing a QR code strategy. According to Wikipedia, 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.

@@ -879,14 +692,14 @@ app.get('/qr/:id', async (req, res) => {

A static QR code directly encodes your data into the QR code pattern itself. When you create a static QR code for a URL, that URL is permanently embedded in the black-and-white squares. The QR code reader decodes the pattern and accesses the content directlyβ€”no intermediate server, no redirect, no tracking.

How Static QR Codes Work

-

Think of a static QR code like printing a phone number on a business card. The phone number is the final informationβ€”there's no lookup service or translation layer. When someone scans the QR code, their device reads the encoded data and immediately processes it (opens the URL, displays the text, connects to WiFi, etc.).

+

Think of a static QR code like printing a phone number on a business card. The phone number is the final informationβ€”there's no lookup service or translation layer. When someone scans the QR code, their device reads the encoded data and immediately processes it (opens the URL, displays the text, opens a location in maps, etc.).

Example: If you create a static QR code for https://www.yourwebsite.com/summer-sale-2025, that exact URL is encoded into the QR code pattern. The QR code scanner extracts this URL and opens it directly.

Common Uses for Static QR Codes

    -
  • WiFi passwords: Encode network credentials for guest access
  • -
  • Business card vCards: Share permanent contact information
  • +
  • Contact cards (vCard): Share permanent contact information on business cards
  • +
  • Location links: Direct links to Google Maps locations for offices or stores
  • App store links: Fixed URLs that never change
  • Bitcoin wallet addresses: Cryptocurrency payment addresses
  • Fixed website URLs: Company homepage, about page, etc.
  • @@ -898,7 +711,7 @@ app.get('/qr/:id', async (req, res) => {
    • βœ… Works forever: No dependency on external servers or subscriptions. Once created, it functions permanently.
    • βœ… Faster scanning: No redirect delayβ€”scanner goes directly to content (typically 100-300ms faster than dynamic).
    • -
    • βœ… Works offline: For content types like WiFi credentials or vCards, no internet connection needed.
    • +
    • βœ… Works offline: For content types like vCards or location data, no internet connection needed for initial scan.
    • βœ… Completely free: No ongoing costs or subscriptions required.
    • βœ… Privacy-friendly: No tracking, no data collection, no third-party involvement.
    • βœ… Simple: What you encode is what you getβ€”no complexity.
    • @@ -1077,9 +890,9 @@ Tracking βœ“ | Editable βœ“ | Analytics βœ“

      When Static QR Codes Excel

      -

      Scenario 1: Coffee Shop WiFi Password

      -

      Situation: Coffee shop wants customers to easily connect to WiFi.

      -

      Why Static: WiFi credentials never change. No tracking needed. QR code works forever even if you stop paying for QR service. Privacy-friendly (no data collection).

      +

      Scenario 1: Business Card Contact Information

      +

      Situation: Professional wants to share their contact details easily at networking events.

      +

      Why Static: 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).

      Cost Savings: $0 forever vs $5-15/month for dynamic QR service = $60-180/year saved.

      Scenario 2: Book Back Cover

      @@ -1111,7 +924,7 @@ Tracking βœ“ | Editable βœ“ | Analytics βœ“

      1. Content Never Changes

        -
      • WiFi password that's permanent
      • +
      • Contact information (vCard) that remains constant
      • App store download link (Apple App Store / Google Play URLs are stable)
      • Company homepage that's been the same for years
      • Historical information (museum exhibits, memorial plaques)
      • @@ -1135,8 +948,8 @@ Tracking βœ“ | Editable βœ“ | Analytics βœ“

        4. Offline Content

          -
        • WiFi credentials (works without internet)
        • vCard contact information (stored locally on device)
        • +
        • Location coordinates (opens maps app directly)
        • Plain text messages or instructions
        • SMS or phone number links
        diff --git a/src/app/(marketing)/blog/page.tsx b/src/app/(marketing)/blog/page.tsx index 77cbf4d..7f480a3 100644 --- a/src/app/(marketing)/blog/page.tsx +++ b/src/app/(marketing)/blog/page.tsx @@ -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 ( <> - +
        +

        QR Code Insights diff --git a/src/app/(marketing)/bulk-qr-code-generator/page.tsx b/src/app/(marketing)/bulk-qr-code-generator/page.tsx index 8599bff..e363378 100644 --- a/src/app/(marketing)/bulk-qr-code-generator/page.tsx +++ b/src/app/(marketing)/bulk-qr-code-generator/page.tsx @@ -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 ( -
        - {/* Hero Section */} -
        -
        -
        -
        -
        - ⚑ - Generate 1000s in Minutes -
        - -

        - Bulk QR Code Generator -

        - -

        - 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. -

        - -
        - {[ - 'Upload Excel or CSV files', - 'Generate up to 1000 QR codes', - 'Custom branding on all codes', - 'Download as organized ZIP', - ].map((feature, index) => ( -
        -
        - - - -
        - {feature} -
        - ))} -
        - -
        - - - - - - -
        -
        - - {/* Visual Example */} -
        - -

        Upload Your File

        -
        -
        πŸ“Š
        -

        products.xlsx

        -

        1,247 rows ready

        + <> + +
        + {/* Hero Section */} +
        +
        + +
        +
        +
        + ⚑ + Generate 1000s in Minutes
        -
        - - - -
        -
        - {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( -
        - QR {i} + +

        + Bulk QR Code Generator +

        + +

        + 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. +

        + +
        + {[ + 'Upload Excel or CSV files', + 'Generate URLs, vCards, locations & more', + 'Custom branding on all codes', + 'Download as organized ZIP', + ].map((feature, index) => ( +
        +
        + + + +
        + {feature}
        ))}
        -

        - + 1,239 more codes -

        - -
        - 1000s at Once! + +
        + + + + + + +
        +
        + + {/* Visual Example */} +
        + +

        Upload Your File

        +
        +
        πŸ“Š
        +

        products.xlsx

        +

        1,247 rows ready

        +
        +
        + + + +
        +
        + {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( +
        + QR {i} +
        + ))} +
        +

        + + 1,239 more codes +

        +
        +
        + 1000s at Once! +
        -
        -
        +
        - {/* Features */} -
        -
        -
        -

        - Powerful Bulk Generation Features -

        -

        - Everything you need to create and manage QR codes at scale -

        -
        + {/* Supported QR Code Types */} +
        +
        +
        +

        + Supported QR Code Types +

        +

        + Generate multiple types of QR codes from your CSV or Excel file. Each type has its own format requirements. +

        +
        -
        - {bulkFeatures.map((feature, index) => ( - -
        {feature.icon}
        -

        - {feature.title} -

        -

        - {feature.description} -

        -
        - ))} -
        -
        -
        - - {/* How It Works */} -
        -
        -
        -

        - How Bulk QR Generation Works -

        -

        - Simple 4-step process to create hundreds of QR codes -

        -
        - -
        - {howItWorks.map((item, index) => ( - -
        -
        - {item.step} -
        -
        -

        - {item.title} -

        -

        - {item.description} -

        -
        -

        - {item.example} -

        +
        + {qrCodeTypes.map((qrType, index) => ( + +
        +
        {qrType.icon}
        +
        +

        + {qrType.title} +

        + {qrType.type}
        -
        - - ))} -
        -
        -
        +

        + {qrType.description} +

        +
        +

        Format:

        + {qrType.format} +
        +
        +

        CSV Example:

        + {qrType.example} +
        + + ))} +
        - {/* File Format Guide */} -
        -
        -
        -

        - CSV/Excel File Format -

        -

        - Simple file structure for bulk QR code generation -

        +
        +

        + πŸ“₯ CSV/Excel File Format +

        +

        + Your file must have at least these three columns: title, contentType, and content +

        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        titlecontentTypecontenttags
        Product PageURLhttps://example.com/productproduct,shop
        John DoeVCARDJohn,Doe,john@example.com,+1234567890,Company,CEOcontact
        Office LocationGEO37.7749,-122.4194,Main Officelocation
        Support HotlinePHONE+1234567890support
        Serial NumberTEXTSN-12345-ABCDEproduct,serial
        +
        +
        +
        - - - - - - - - - - - {fileFormat.map((field, index) => ( - - - - + {/* Features */} +
        +
        +
        +

        + Powerful Bulk Generation Features +

        +

        + Everything you need to create and manage QR codes at scale +

        +
        + +
        + {bulkFeatures.map((feature, index) => ( + +
        {feature.icon}
        +

        + {feature.title} +

        +

        + {feature.description} +

        +
        + ))} +
        +
        +
        + + {/* How It Works */} +
        +
        +
        +

        + How Bulk QR Generation Works +

        +

        + Simple 4-step process to create hundreds of QR codes +

        +
        + +
        + {howItWorks.map((item, index) => ( + +
        +
        + {item.step} +
        +
        +

        + {item.title} +

        +

        + {item.description} +

        +
        +

        + {item.example} +

        +
        +
        +
        +
        + ))} +
        +
        +
        + + {/* File Format Guide */} +
        +
        +
        +

        + CSV/Excel File Format +

        +

        + Simple file structure for bulk QR code generation +

        +
        + + +
        ColumnDescriptionRequired
        - - {field.column} - - {field.description} - {field.required ? ( - Yes - ) : ( - No - )} -
        + + + + + - ))} - -
        ColumnDescriptionRequired
        -
        + +

+ + {field.column} + + {field.description} + {field.required ? ( + Yes + ) : ( + No + )} +
+ - -

Example CSV:

-
-{`name,url,description,tags
+            
+              

Example CSV:

+
+                {`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`}
-            
-
- - - - {/* Use Cases */} -
-
-
-

- Bulk QR Code Use Cases -

-

- Industries and scenarios where bulk generation shines -

+
+
+ -
- {useCases.map((useCase, index) => ( - -
-
{useCase.icon}
-
-

- {useCase.title} -

-

- {useCase.description} -

-
    - {useCase.stats.map((stat, idx) => ( -
  • - - - - {stat} -
  • - ))} -
+ {/* Use Cases */} +
+
+
+

+ Bulk QR Code Use Cases +

+

+ Industries and scenarios where bulk generation shines +

+
+ +
+ {useCases.map((useCase, index) => ( + +
+
{useCase.icon}
+
+

+ {useCase.title} +

+

+ {useCase.description} +

+
    + {useCase.stats.map((stat, idx) => ( +
  • + + + + {stat} +
  • + ))} +
+
-
- - ))} + + ))} +
-
- + - {/* CTA Section */} -
-
-

- Generate 1000s of QR Codes in Minutes -

-

- Save hours of manual work. Upload your file and get all QR codes ready instantly. -

-
- - - - - - + {/* CTA Section */} +
+
+

+ Generate 1000s of QR Codes in Minutes +

+

+ Save hours of manual work. Upload your file and get all QR codes ready instantly. +

+
+ + + + + + +
-
-
-
+ + + ); } diff --git a/src/app/(marketing)/dynamic-qr-code-generator/page.tsx b/src/app/(marketing)/dynamic-qr-code-generator/page.tsx index 1b66a88..06ac40e 100644 --- a/src/app/(marketing)/dynamic-qr-code-generator/page.tsx +++ b/src/app/(marketing)/dynamic-qr-code-generator/page.tsx @@ -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 ( -
- {/* Hero Section */} -
-
-
-
-
- ✨ - Edit After Printing + <> + +
+ {/* Hero Section */} +
+
+ +
+
+
+ ✨ + Edit After Printing +
+ +

+ Dynamic QR Code Generator +

+ +

+ Create QR codes you can edit anytime - even after printing. Change URLs, track scans, and update content without reprinting. The smart choice for businesses. +

+ +
+ {[ + 'Edit content after printing', + 'Track scans and analytics', + 'A/B test without reprinting', + 'Custom branding and design', + ].map((feature, index) => ( +
+
+ + + +
+ {feature} +
+ ))} +
+ +
+ + + + + + +
-

- Dynamic QR Code Generator -

- -

- Create QR codes you can edit anytime - even after printing. Change URLs, track scans, and update content without reprinting. The smart choice for businesses. -

- -
- {[ - 'Edit content after printing', - 'Track scans and analytics', - 'A/B test without reprinting', - 'Custom branding and design', - ].map((feature, index) => ( -
-
- - + {/* Visual Demo */} +
+ +
+
+
+ QR Code +
+
+
+
+
+ Current URL: + summer-sale.com +
+
+ +
- {feature} -
- ))} -
- -
- - - - - - -
-
- - {/* Visual Demo */} -
- -
-
-
- QR Code +
+ Updated URL: + fall-sale.com
+

+ Same QR code, different destination! +

+ +
+ No Reprint Needed!
-
-
- Current URL: - summer-sale.com -
-
- - - -
-
- Updated URL: - fall-sale.com -
-
-

- Same QR code, different destination! -

- -
- No Reprint Needed!
-
-
+
- {/* Static vs Dynamic */} -
-
-
-

- Dynamic vs Static QR Codes -

-

- Understand why dynamic QR codes are the smart choice for businesses -

-
- - -
-
-

Feature

- {staticVsDynamic.map((item, index) => ( -
-

{item.feature}

-
- ))} -
-
-

Static QR

- {staticVsDynamic.map((item, index) => ( -
- {item.static ? ( - βœ“ - ) : ( - βœ— - )} -
- ))} -
-
-

Dynamic QR

- {staticVsDynamic.map((item, index) => ( -
- {item.dynamic ? ( - βœ“ - ) : ( - βœ— - )} -
- ))} -
+ {/* Static vs Dynamic */} +
+
+
+

+ Dynamic vs Static QR Codes +

+

+ Understand why dynamic QR codes are the smart choice for businesses +

- -
-
- {/* Features */} -
-
-
-

- Powerful Dynamic QR Features -

-

- Everything you need to create, manage, and optimize your QR code campaigns -

+ +
+
+

Feature

+ {staticVsDynamic.map((item, index) => ( +
+

{item.feature}

+
+ ))} +
+
+

Static QR

+ {staticVsDynamic.map((item, index) => ( +
+ {item.static ? ( + βœ“ + ) : ( + βœ— + )} +
+ ))} +
+
+

Dynamic QR

+ {staticVsDynamic.map((item, index) => ( +
+ {item.dynamic ? ( + βœ“ + ) : ( + βœ— + )} +
+ ))} +
+
+
+
-
- {dynamicFeatures.map((feature, index) => ( - -
{feature.icon}
-

- {feature.title} -

-

- {feature.description} -

-
- ))} + {/* Features */} +
+
+
+

+ Powerful Dynamic QR Features +

+

+ Everything you need to create, manage, and optimize your QR code campaigns +

+
+ +
+ {dynamicFeatures.map((feature, index) => ( + +
{feature.icon}
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
-
-
+ - {/* Use Cases */} -
-
-
-

- How Businesses Use Dynamic QR Codes -

-

- Real-world examples of dynamic QR code applications -

-
+ {/* Use Cases */} +
+
+
+

+ How Businesses Use Dynamic QR Codes +

+

+ Real-world examples of dynamic QR code applications +

+
-
- {useCases.map((useCase, index) => ( - -
-
{useCase.icon}
-
-

- {useCase.title} -

-

- {useCase.description} -

-
-

- Example: {useCase.example} +

+ {useCases.map((useCase, index) => ( + +
+
{useCase.icon}
+
+

+ {useCase.title} +

+

+ {useCase.description}

+
+

+ Example: {useCase.example} +

+
+
+ ))} +
+
+
+ + {/* How It Works */} +
+
+
+

+ How Dynamic QR Codes Work +

+

+ Simple technology, powerful results +

+
+ +
+ +
+ 1
+

Create QR Code

+

+ Generate a dynamic QR code with a short redirect URL +

- ))} -
-
-
- {/* How It Works */} -
-
-
-

- How Dynamic QR Codes Work + +
+ 2 +
+

Print Anywhere

+

+ Add to packaging, posters, cards, or anywhere you need +

+
+ + +
+ 3 +
+

Update Anytime

+

+ Change the destination URL from your dashboard whenever needed +

+
+

+
+
+ + {/* CTA Section */} +
+
+

+ Start Creating Dynamic QR Codes Today

-

- Simple technology, powerful results +

+ Join thousands of businesses who never worry about reprinting QR codes again

+
+ + + + + + +
- -
- -
- 1 -
-

Create QR Code

-

- Generate a dynamic QR code with a short redirect URL -

-
- - -
- 2 -
-

Print Anywhere

-

- Add to packaging, posters, cards, or anywhere you need -

-
- - -
- 3 -
-

Update Anytime

-

- Change the destination URL from your dashboard whenever needed -

-
-
-
-
- - {/* CTA Section */} -
-
-

- Start Creating Dynamic QR Codes Today -

-

- Join thousands of businesses who never worry about reprinting QR codes again -

-
- - - - - - -
-
-
-
+ + + ); } diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index 079da24..8603b1b 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -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({
{/* Logo */} - QR Master + QR Master QR Master @@ -111,7 +110,7 @@ export default function MarketingLayout({
- QR Master + QR Master QR Master

@@ -138,14 +137,6 @@ export default function MarketingLayout({

  • Get Started
  • - -
    -

    Legal

    -
      -
    • Privacy Policy
    • -
    • Terms of Service
    • -
    -
    @@ -153,9 +144,6 @@ export default function MarketingLayout({
    - - {/* Cookie Banner */} - ); } \ No newline at end of file diff --git a/src/app/(marketing)/qr-code-tracking/page.tsx b/src/app/(marketing)/qr-code-tracking/page.tsx index 97dff59..f3f8c52 100644 --- a/src/app/(marketing)/qr-code-tracking/page.tsx +++ b/src/app/(marketing)/qr-code-tracking/page.tsx @@ -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 ( -
    - {/* Hero Section */} -
    -
    -
    -
    -
    - πŸ“Š - Free QR Code Tracking + <> + +
    + {/* Hero Section */} +
    +
    + +
    +
    +
    + πŸ“Š + Free QR Code Tracking +
    + +

    + Track Every QR Code Scan with Powerful Analytics +

    + +

    + 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. +

    + +
    + + + + + + +
    + +
    +
    + + + + No credit card required +
    +
    + + + + Unlimited scans +
    +
    -

    - Track Every QR Code Scan with Powerful Analytics -

    + {/* Analytics Preview */} +
    + +

    Live Analytics Dashboard

    +
    +
    + Total Scans + 12,547 +
    +
    + Unique Users + 8,392 +
    +
    + Top Location + πŸ‡©πŸ‡ͺ Germany +
    +
    + Top Device + πŸ“± iPhone +
    +
    +
    +
    + Live Updates +
    +
    +
    +
    +
    -

    - 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 */} +

    +
    +
    +

    + Powerful QR Code Tracking Features +

    +

    + Get complete visibility into your QR code performance with our comprehensive analytics suite

    - -
    - - - - - - -
    - -
    -
    - - - - No credit card required -
    -
    - - - - Unlimited scans -
    -
    - {/* Analytics Preview */} -
    - -

    Live Analytics Dashboard

    -
    -
    - Total Scans - 12,547 -
    -
    - Unique Users - 8,392 -
    -
    - Top Location - πŸ‡©πŸ‡ͺ Germany -
    -
    - Top Device - πŸ“± iPhone -
    -
    -
    -
    - Live Updates -
    +
    + {trackingFeatures.map((feature, index) => ( + +
    {feature.icon}
    +

    + {feature.title} +

    +

    + {feature.description} +

    +
    + ))}
    -
    -
    +
    - {/* Tracking Features */} -
    -
    -
    -

    - Powerful QR Code Tracking Features -

    -

    - Get complete visibility into your QR code performance with our comprehensive analytics suite -

    + {/* Use Cases */} +
    +
    +
    +

    + QR Code Tracking Use Cases +

    +

    + See how businesses use QR code tracking to improve their operations +

    +
    + +
    + {useCases.map((useCase, index) => ( + +

    + {useCase.title} +

    +

    + {useCase.description} +

    +
      + {useCase.benefits.map((benefit, idx) => ( +
    • + + + + {benefit} +
    • + ))} +
    +
    + ))} +
    +
    -
    - {trackingFeatures.map((feature, index) => ( - -
    {feature.icon}
    -

    - {feature.title} -

    -

    - {feature.description} -

    -
    - ))} -
    -
    -
    + {/* Comparison Table */} +
    +
    +
    +

    + QR Master vs Free Tools +

    +

    + See why businesses choose QR Master for QR code tracking +

    +
    - {/* Use Cases */} -
    -
    -
    -

    - QR Code Tracking Use Cases -

    -

    - See how businesses use QR code tracking to improve their operations -

    -
    - -
    - {useCases.map((useCase, index) => ( - -

    - {useCase.title} -

    -

    - {useCase.description} -

    -
      - {useCase.benefits.map((benefit, idx) => ( -
    • - - - - {benefit} -
    • - ))} -
    -
    - ))} -
    -
    -
    - - {/* Comparison Table */} -
    -
    -
    -

    - QR Master vs Free Tools -

    -

    - See why businesses choose QR Master for QR code tracking -

    -
    - - - - - - - - - - - - {comparisonData.map((row, index) => ( - - - + + ))} + +
    FeatureFree ToolsQR Master
    {row.feature} - {typeof row.free === 'boolean' ? ( - row.free ? ( + + + + + + + + + + + {comparisonData.map((row, index) => ( + + + + - - - ))} - -
    FeatureFree ToolsQR Master
    {row.feature} + {typeof row.free === 'boolean' ? ( + row.free ? ( + βœ“ + ) : ( + βœ— + ) + ) : ( + {row.free} + )} + + {typeof row.qrMaster === 'boolean' ? ( βœ“ ) : ( - βœ— - ) - ) : ( - {row.free} - )} - - {typeof row.qrMaster === 'boolean' ? ( - βœ“ - ) : ( - {row.qrMaster} - )} -
    -
    - - - - {/* CTA Section */} -
    -
    -

    - Start Tracking Your QR Codes Today -

    -

    - Join thousands of businesses using QR Master to track and optimize their QR code campaigns -

    -
    - - - - - - + {row.qrMaster} + )} +
    +
    -
    -
    -
    + + + {/* CTA Section */} +
    +
    +

    + Start Tracking Your QR Codes Today +

    +

    + Join thousands of businesses using QR Master to track and optimize their QR code campaigns +

    +
    + + + + + + +
    +
    +
    + + ); } diff --git a/src/app/api/auth/google/route.ts b/src/app/api/auth/google/route.ts index fab94ed..54d6053 100644 --- a/src/app/api/auth/google/route.ts +++ b/src/app/api/auth/google/route.ts @@ -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( diff --git a/src/app/api/bulk/route.ts b/src/app/api/bulk/route.ts deleted file mode 100644 index 7defdc6..0000000 --- a/src/app/api/bulk/route.ts +++ /dev/null @@ -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 } - ); - } -} \ No newline at end of file diff --git a/src/app/api/qrs/route.ts b/src/app/api/qrs/route.ts index 07e7c36..68bf722 100644 --- a/src/app/api/qrs/route.ts +++ b/src/app/api/qrs/route.ts @@ -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; diff --git a/src/app/api/qrs/static/route.ts b/src/app/api/qrs/static/route.ts index f98c51a..5cb6446 100644 --- a/src/app/api/qrs/static/route.ts +++ b/src/app/api/qrs/static/route.ts @@ -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; diff --git a/src/app/r/[slug]/route.ts b/src/app/r/[slug]/route.ts index 9708bd7..a65a4b5 100644 --- a/src/app/r/[slug]/route.ts +++ b/src/app/r/[slug]/route.ts @@ -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 diff --git a/src/app/vcard/page.tsx b/src/app/vcard/page.tsx new file mode 100644 index 0000000..9caad7c --- /dev/null +++ b/src/app/vcard/page.tsx @@ -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 ( +
    +
    +
    πŸ‘€
    +

    Loading contact...

    +
    +
    + ); + } + + if (!firstName && !lastName) { + return ( +
    +
    +
    πŸ‘€
    +

    No Contact Found

    +

    This QR code doesn't contain any contact information.

    +
    +
    + ); + } + + return ( +
    +
    + {/* Header */} +
    +
    πŸ‘€
    + +

    + {firstName} {lastName} +

    + + {(title || organization) && ( +

    + {title && organization && `${title} at ${organization}`} + {title && !organization && title} + {!title && organization && organization} +

    + )} +
    + + {/* Contact Details */} +
    + {email && ( +
    +
    Email
    + {email} +
    + )} + + {phone && ( +
    +
    Phone
    + {phone} +
    + )} +
    + + {/* Save Button */} + + +

    + Add this contact to your address book +

    +
    +
    + ); +} diff --git a/src/components/dashboard/QRCodeCard.tsx b/src/components/dashboard/QRCodeCard.tsx index 63233a3..7c19a46 100644 --- a/src/components/dashboard/QRCodeCard.tsx +++ b/src/components/dashboard/QRCodeCard.tsx @@ -45,8 +45,22 @@ export const QRCodeCard: React.FC = ({ 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) { diff --git a/src/components/marketing/Hero.tsx b/src/components/marketing/Hero.tsx index adbd71c..c9ebfef 100644 --- a/src/components/marketing/Hero.tsx +++ b/src/components/marketing/Hero.tsx @@ -13,13 +13,13 @@ interface HeroProps { export const Hero: React.FC = ({ 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 ( -
    +
    {/* Left Content */} diff --git a/src/components/marketing/Pricing.tsx b/src/components/marketing/Pricing.tsx index 2975697..b5f3282 100644 --- a/src/components/marketing/Pricing.tsx +++ b/src/components/marketing/Pricing.tsx @@ -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 = ({ t }) => { ))} - + + + ))}
    - -
    ); diff --git a/src/components/marketing/TemplateCards.tsx b/src/components/marketing/TemplateCards.tsx index fba2e2a..a8b97c4 100644 --- a/src/components/marketing/TemplateCards.tsx +++ b/src/components/marketing/TemplateCards.tsx @@ -25,9 +25,9 @@ export const TemplateCards: React.FC = ({ 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', }, diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 417572d..6861c9f 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -7,7 +7,17 @@ interface InputProps extends React.InputHTMLAttributes { } export const Input = React.forwardRef( - ({ className, type, label, error, ...props }, ref) => { + ({ className, type, label, error, onInvalid, ...props }, ref) => { + // Default English validation message + const handleInvalid = (e: React.InvalidEvent) => { + e.target.setCustomValidity('Please fill out this field.'); + if (onInvalid) onInvalid(e); + }; + + const handleInput = (e: React.FormEvent) => { + e.currentTarget.setCustomValidity(''); + }; + return (
    {label && ( @@ -23,6 +33,8 @@ export const Input = React.forwardRef( className )} ref={ref} + onInvalid={handleInvalid} + onInput={handleInput} {...props} /> {error && ( diff --git a/src/i18n/de.json b/src/i18n/de.json index 88fdcee..2189fca 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -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 β†’" }, diff --git a/src/i18n/en.json b/src/i18n/en.json index b6735da..05e41c7 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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 β†’" }, diff --git a/src/lib/qr.ts b/src/lib/qr.ts index 3267be6..ff15f2e 100644 --- a/src/lib/qr.ts +++ b/src/lib/qr.ts @@ -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: diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts index 69f2554..f754cc7 100644 --- a/src/lib/stripe.ts +++ b/src/lib/stripe.ts @@ -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, diff --git a/src/lib/validationSchemas.ts b/src/lib/validationSchemas.ts index 97b479f..9aea3e1 100644 --- a/src/lib/validationSchemas.ts +++ b/src/lib/validationSchemas.ts @@ -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'),