diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ddc582c..96c001f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,8 @@ "Bash(ls:*)", "Bash(curl:*)", "Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")", - "Bash(pkill:*)" + "Bash(pkill:*)", + "Skill(shadcn-ui)" ], "deny": [], "ask": [] diff --git a/README.md b/README.md index fda3558..7b1ca43 100644 --- a/README.md +++ b/README.md @@ -42,43 +42,53 @@ A production-ready SaaS application for creating and managing QR codes with adva Run database in Docker, app on host machine: 1. Clone the repository: + ```bash git clone https://github.com/yourusername/qr-master.git cd qr-master ``` 2. Install dependencies: + ```bash npm install ``` 3. Copy and configure environment: + ```bash cp env.example .env ``` + Edit `.env` and set: - - `NEXTAUTH_SECRET` (generate: `openssl rand -base64 32`) - - `IP_SALT` (generate: `openssl rand -base64 32`) - - (Optional) Google OAuth credentials + +- `NEXTAUTH_SECRET` (generate: `openssl rand -base64 32`) +- `IP_SALT` (generate: `openssl rand -base64 32`) +- (Optional) Google OAuth credentials 4. Start database services: + ```bash npm run docker:dev ``` 5. Run database migrations and seed: + ```bash 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 ``` @@ -94,6 +104,7 @@ npm run dev Run everything in Docker: 1. Clone and setup: + ```bash git clone https://github.com/yourusername/qr-master.git cd qr-master @@ -102,11 +113,13 @@ cp env.example .env ``` 2. Build and start: + ```bash npm run docker:prod ``` 3. Run migrations: + ```bash docker-compose exec web npx prisma migrate deploy ``` @@ -158,6 +171,7 @@ npm run docker:backup # Backup database to SQL file ### Local Development (without Docker) 1. Install dependencies: + ```bash npm install ``` @@ -165,17 +179,20 @@ npm install 2. Set up PostgreSQL and Redis locally 3. Configure `.env` with local database URL: + ```env DATABASE_URL=postgresql://postgres:postgres@localhost:5435/qrmaster?schema=public ``` 4. Run migrations and seed: + ```bash npx prisma migrate dev npm run db:seed ``` 5. Start dev server: + ```bash npm run dev ``` @@ -194,6 +211,7 @@ 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 @@ -224,11 +242,13 @@ qr-master/ ## API Endpoints ### Authentication + - `POST /api/auth/signin` - Sign in with credentials - `POST /api/auth/signout` - Sign out - `GET /api/auth/session` - Get current session ### QR Codes + - `GET /api/qrs` - List all QR codes - `POST /api/qrs` - Create a new QR code (dynamic or static) - `POST /api/qrs/static` - Create a static QR code @@ -238,9 +258,11 @@ qr-master/ - `DELETE /api/qrs/delete-all` - Delete all user's QR codes ### Analytics + - `GET /api/analytics/summary` - Get analytics summary for a QR code ### User & Settings + - `GET /api/user/plan` - Get current user plan - `GET /api/user/stats` - Get user statistics - `POST /api/user/password` - Update password @@ -248,31 +270,33 @@ qr-master/ - `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 ## Environment Variables -| Variable | Description | Required | Default | -|----------|-------------|----------|---------| -| `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 (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` | -| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog analytics key | No | - | -| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host URL | No | - | +| Variable | Description | Required | Default | +| ------------------------------------ | ----------------------------- | -------- | ---------------------------------------------------------------------- | +| `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 (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` | +| `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. @@ -356,12 +380,14 @@ For detailed deployment instructions, see [DOCKER_SETUP.md](DOCKER_SETUP.md). ### 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 @@ -372,12 +398,14 @@ 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 @@ -394,6 +422,7 @@ 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 @@ -406,12 +435,14 @@ taskkill /PID /F ### 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 @@ -431,7 +462,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Support -For support, email support@qrmaster.com or open an issue on GitHub. +For support, email support@qrmaster.net or open an issue on GitHub. ## Acknowledgments @@ -441,4 +472,4 @@ For support, email support@qrmaster.com or open an issue on GitHub. --- -Built with ❤️ by QR Master Team \ No newline at end of file +Built with ❤️ by QR Master Team diff --git a/next-sitemap.config.js b/next-sitemap.config.js index fca2437..9441a1f 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,6 +1,6 @@ /** @type {import('next-sitemap').IConfig} */ module.exports = { - siteUrl: 'https://www.qrmaster.com', + siteUrl: 'https://www.qrmaster.net', generateRobotsTxt: true, robotsTxtOptions: { policies: [ diff --git a/next.config.mjs b/next.config.mjs index 6a8ea98..33539d1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,14 +3,14 @@ const nextConfig = { output: 'standalone', images: { unoptimized: false, - domains: ['www.qrmaster.com', 'qrmaster.com', 'images.qrmaster.com'], + domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'], formats: ['image/webp', 'image/avif'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, experimental: { - serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'] - } -} + serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'], + }, +}; -export default nextConfig \ No newline at end of file +export default nextConfig; diff --git a/prisma/seed.ts b/prisma/seed.ts index 08d2a7f..3caacbe 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -8,10 +8,10 @@ async function main() { const hashedPassword = await bcrypt.hash('demo123', 12); const user = await prisma.user.upsert({ - where: { email: 'demo@qrmaster.com' }, + where: { email: 'demo@qrmaster.net' }, update: {}, create: { - email: 'demo@qrmaster.com', + email: 'demo@qrmaster.net', name: 'Demo User', password: hashedPassword, }, diff --git a/public/sitemap.xml b/public/sitemap.xml index 0fa6818..d3ac98c 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,31 +1,31 @@ - https://www.qrmaster.com/ + https://www.qrmaster.net/ 2025-10-16T00:00:00Z daily 0.9 - https://www.qrmaster.com/blog + https://www.qrmaster.net/blog 2025-10-16T00:00:00Z daily 0.7 - https://www.qrmaster.com/pricing + https://www.qrmaster.net/pricing 2025-10-16T00:00:00Z weekly 0.8 - https://www.qrmaster.com/faq + https://www.qrmaster.net/faq 2025-10-16T00:00:00Z weekly 0.6 - https://www.qrmaster.com/blog/qr-code-analytics + https://www.qrmaster.net/blog/qr-code-analytics 2025-10-16T00:00:00Z weekly 0.6 diff --git a/src/app/(app)/analytics/page.tsx b/src/app/(app)/analytics/page.tsx index 41b4f88..64e716f 100644 --- a/src/app/(app)/analytics/page.tsx +++ b/src/app/(app)/analytics/page.tsx @@ -344,7 +344,13 @@ export default function AnalyticsPage() { {country.count.toLocaleString()} {country.percentage}% - + + {country.trend === 'up' ? '↑' : country.trend === 'down' ? '↓' : '→'} {country.trendPercentage}% + ))} @@ -387,8 +393,12 @@ export default function AnalyticsPage() { {qr.uniqueScans.toLocaleString()} {qr.conversion}% - 0 ? 'success' : 'default'}> - {qr.totalScans > 0 ? '↑' : '—'} + + {qr.trend === 'up' ? '↑' : qr.trend === 'down' ? '↓' : '→'} {qr.trendPercentage}% diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 6fda613..3ec435d 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -20,7 +20,6 @@ interface QRCodeData { contentType: string; content?: any; slug: string; - status: 'ACTIVE' | 'PAUSED'; createdAt: string; scans: number; style?: any; @@ -219,36 +218,6 @@ export default function DashboardPage() { router.push(`/qr/${id}/edit`); }; - const handlePause = async (id: string) => { - try { - const qr = qrCodes.find(q => q.id === id); - if (!qr) return; - - const newStatus = qr.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE'; - - const response = await fetch(`/api/qrs/${id}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ status: newStatus }), - }); - - if (response.ok) { - // Update local state - setQrCodes(qrCodes.map(q => - q.id === id ? { ...q, status: newStatus } : q - )); - showToast(`QR code ${newStatus === 'ACTIVE' ? 'resumed' : 'paused'}!`, 'success'); - } else { - throw new Error('Failed to update status'); - } - } catch (error) { - console.error('Error updating QR status:', error); - showToast('Failed to update QR code status', 'error'); - } - }; - const handleDelete = async (id: string) => { if (!confirm('Are you sure you want to delete this QR code? This action cannot be undone.')) { return; @@ -393,7 +362,6 @@ export default function DashboardPage() { key={qr.id} qr={qr} onEdit={handleEdit} - onPause={handlePause} onDelete={handleDelete} /> ))} diff --git a/src/app/(app)/pricing/page.tsx b/src/app/(app)/pricing/page.tsx index 747da75..86d11f1 100644 --- a/src/app/(app)/pricing/page.tsx +++ b/src/app/(app)/pricing/page.tsx @@ -6,11 +6,14 @@ import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { showToast } from '@/components/ui/Toast'; import { useRouter } from 'next/navigation'; +import { BillingToggle } from '@/components/ui/BillingToggle'; export default function PricingPage() { const router = useRouter(); const [loading, setLoading] = useState(null); const [currentPlan, setCurrentPlan] = useState('FREE'); + const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null); + const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); useEffect(() => { // Fetch current user plan @@ -20,6 +23,7 @@ export default function PricingPage() { if (response.ok) { const data = await response.json(); setCurrentPlan(data.plan || 'FREE'); + setCurrentInterval(data.interval || null); } } catch (error) { console.error('Error fetching user plan:', error); @@ -40,7 +44,7 @@ export default function PricingPage() { }, body: JSON.stringify({ plan, - billingInterval: 'month', + billingInterval: billingPeriod === 'month' ? 'month' : 'year', }), }); @@ -95,17 +99,31 @@ export default function PricingPage() { } }; + // Helper function to check if this is the user's exact current plan (plan + interval) + const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => { + return currentPlan === planType && currentInterval === interval; + }; + + // Helper function to check if user has this plan but different interval + const hasPlanDifferentInterval = (planType: string) => { + return currentPlan === planType && currentInterval && currentInterval !== billingPeriod; + }; + + const selectedInterval = billingPeriod === 'month' ? 'month' : 'year'; + const plans = [ { key: 'free', name: 'Free', price: '€0', period: 'forever', + showDiscount: false, features: [ '3 dynamic QR codes', 'Unlimited static QR codes', 'Basic scan tracking', 'Standard QR design templates', + 'Download as SVG/PNG', ], buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free', buttonVariant: 'outline' as const, @@ -116,26 +134,31 @@ export default function PricingPage() { { key: 'pro', name: 'Pro', - price: '€9', - period: 'per month', + price: billingPeriod === 'month' ? '€9' : '€90', + period: billingPeriod === 'month' ? 'per month' : 'per year', + showDiscount: billingPeriod === 'year', features: [ '50 dynamic QR codes', 'Unlimited static QR codes', 'Advanced analytics (scans, devices, locations)', 'Custom branding (colors)', - 'Download as SVG/PNG', ], - buttonText: currentPlan === 'PRO' ? 'Current Plan' : 'Upgrade to Pro', + buttonText: isCurrentPlanWithInterval('PRO', selectedInterval) + ? 'Current Plan' + : hasPlanDifferentInterval('PRO') + ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` + : 'Upgrade to Pro', buttonVariant: 'primary' as const, - disabled: currentPlan === 'PRO', + disabled: isCurrentPlanWithInterval('PRO', selectedInterval), popular: true, onUpgrade: () => handleUpgrade('PRO'), }, { key: 'business', name: 'Business', - price: '€29', - period: 'per month', + price: billingPeriod === 'month' ? '€29' : '€290', + period: billingPeriod === 'month' ? 'per month' : 'per year', + showDiscount: billingPeriod === 'year', features: [ '500 dynamic QR codes', 'Unlimited static QR codes', @@ -144,9 +167,13 @@ export default function PricingPage() { 'Priority email support', 'Advanced tracking & insights', ], - buttonText: currentPlan === 'BUSINESS' ? 'Current Plan' : 'Upgrade to Business', + buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval) + ? 'Current Plan' + : hasPlanDifferentInterval('BUSINESS') + ? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}` + : 'Upgrade to Business', buttonVariant: 'primary' as const, - disabled: currentPlan === 'BUSINESS', + disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval), popular: false, onUpgrade: () => handleUpgrade('BUSINESS'), }, @@ -163,6 +190,10 @@ export default function PricingPage() {

+
+ +
+
{plans.map((plan) => ( {plan.name} -
- - {plan.price} - - - {plan.period} - +
+
+ + {plan.price} + + + {plan.period} + +
+ {plan.showDiscount && ( + + Save 16% + + )}
@@ -222,7 +260,7 @@ export default function PricingPage() { All plans include unlimited static QR codes and basic customization.

- Need help choosing? Contact our team + Need help choosing? Contact our team

diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 155cbbc..a22f552 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -65,6 +65,9 @@ export default function LoginPage() {

Welcome Back

Sign in to your account

+ + ← Back to Home + @@ -158,10 +161,6 @@ export default function LoginPage() {

By signing in, you agree to our{' '} - - Terms of Service - {' '} - and{' '} Privacy Policy diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 87ad2c1..adbaedd 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -77,6 +77,9 @@ export default function SignupPage() {

Create Account

Start creating QR codes in seconds

+ + ← Back to Home + @@ -124,26 +127,6 @@ export default function SignupPage() { required /> -
- (e.target as HTMLInputElement).setCustomValidity('Please check this box if you want to proceed')} - onInput={(e) => (e.target as HTMLInputElement).setCustomValidity('')} - /> - -
- @@ -195,6 +178,13 @@ export default function SignupPage() {
+ +

+ By signing up, you agree to our{' '} + + Privacy Policy + +

); diff --git a/src/app/(marketing)/blog/[slug]/page.tsx b/src/app/(marketing)/blog/[slug]/page.tsx index 7e121ad..a0dfe09 100644 --- a/src/app/(marketing)/blog/[slug]/page.tsx +++ b/src/app/(marketing)/blog/[slug]/page.tsx @@ -40,7 +40,7 @@ const blogPosts: Record = { image: '/blog/4-hero.png', imageAlt: 'Smartphone displaying QR code scan with modern tech aesthetic', author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.com/about', + authorUrl: 'https://www.qrmaster.net/about', answer: 'QR code analytics empowers marketers to track scan rates, user behavior, and campaign ROI through real-time dashboards, enabling data-driven optimization of dynamic QR codes and branded marketing campaigns.', howTo: { name: 'How to Track QR Code Scans', @@ -50,7 +50,7 @@ const blogPosts: Record = { { name: 'Create a Dynamic QR Code', text: 'Log into your QR Master dashboard and select "Create Dynamic QR Code". Enter your destination URL and customize design options.', - url: 'https://www.qrmaster.com/create', + url: 'https://www.qrmaster.net/create', }, { name: 'Enable UTM Tracking', @@ -59,7 +59,7 @@ const blogPosts: Record = { { name: 'Access Analytics Dashboard', text: 'Navigate to Dashboard → Analytics to view real-time scan data, geographic distribution, and device breakdowns.', - url: 'https://www.qrmaster.com/analytics', + url: 'https://www.qrmaster.net/analytics', }, ], }, @@ -156,7 +156,7 @@ const blogPosts: Record = { image: '/blog/1-hero.png', imageAlt: 'QR code tracking and analytics visualization', author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.com/about', + authorUrl: 'https://www.qrmaster.net/about', answer: 'QR code tracking allows you to monitor scan metrics including location, device type, time, and user behavior using dynamic QR codes. Only dynamic QR codes can be tracked—static codes cannot provide analytics. Use tools like QR Master, Google Analytics with UTM parameters, or URL shorteners to track scans and measure campaign ROI effectively.', howTo: { name: 'How to Set Up QR Code Tracking', @@ -166,7 +166,7 @@ const blogPosts: Record = { { name: 'Create a Dynamic QR Code', text: 'Sign up for QR Master and create a dynamic QR code. Enter your destination URL and customize the design with your brand colors and logo.', - url: 'https://www.qrmaster.com/signup', + url: 'https://www.qrmaster.net/signup', }, { name: 'Add UTM Parameters', @@ -179,7 +179,7 @@ const blogPosts: Record = { { name: 'Monitor Analytics Dashboard', text: 'Access your QR Master dashboard to view real-time scan data: total scans, unique users, geographic location, device types, and scan timestamps.', - url: 'https://www.qrmaster.com/analytics', + url: 'https://www.qrmaster.net/analytics', }, { name: 'Optimize Based on Data', @@ -212,7 +212,7 @@ const blogPosts: Record = {

Static QR Codes: These encode the destination URL directly into the QR code pattern. Once generated, the content cannot be changed, and no tracking is possible. The QR code reader goes directly to the encoded destination without any intermediate server.

-

Dynamic QR Codes: These contain a short redirect URL (like qrmaster.com/abc123) that points to a server. The server logs the scan data and then redirects to your actual destination URL. This enables tracking AND allows you to change the destination URL anytime—even after printing thousands of codes.

+

Dynamic QR Codes: These contain a short redirect URL (like qrmaster.net/abc123) that points to a server. The server logs the scan data and then redirects to your actual destination URL. This enables tracking AND allows you to change the destination URL anytime—even after printing thousands of codes.

@@ -276,7 +276,7 @@ const blogPosts: Record = {

Step-by-Step with QR Master:

    -
  1. Sign up for free: Create your QR Master account at qrmaster.com/signup
  2. +
  3. Sign up for free: Create your QR Master account at qrmaster.net/signup
  4. Create dynamic QR code: Click "Create QR Code" and select "Dynamic QR"
  5. Enter destination URL: Add the website, landing page, or content you want to link
  6. Customize design: Add your logo, brand colors, and custom frame
  7. @@ -554,10 +554,10 @@ app.get('/qr/:id', async (req, res) => {

    Multi-Channel Attribution

    Use unique QR codes for each marketing channel to measure which drives the best results:

      -
    • Billboard: qrmaster.com/billboard-nyc
    • -
    • Magazine ad: qrmaster.com/magazine-vogue
    • -
    • Product packaging: qrmaster.com/packaging-productA
    • -
    • Business card: qrmaster.com/card-john
    • +
    • Billboard: qrmaster.net/billboard-nyc
    • +
    • Magazine ad: qrmaster.net/magazine-vogue
    • +
    • Product packaging: qrmaster.net/packaging-productA
    • +
    • Business card: qrmaster.net/card-john

    Track scans separately to calculate ROI per channel.

    @@ -680,7 +680,7 @@ app.get('/qr/:id', async (req, res) => { image: '/blog/2-hero.png', imageAlt: 'Two QR codes side by side showing static and dynamic comparison', author: 'QR Master Team', - authorUrl: 'https://www.qrmaster.com/about', + authorUrl: 'https://www.qrmaster.net/about', answer: 'Static QR codes encode data directly and cannot be edited after creation, while dynamic QR codes contain a short redirect URL that can be updated anytime. Dynamic QR codes also provide tracking analytics, making them ideal for marketing campaigns. Static QR codes work forever without subscriptions, perfect for permanent content like contact cards or fixed URLs.', content: `

    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.

    @@ -751,7 +751,7 @@ No Tracking | Cannot Edit | Works Forever

    How Dynamic QR Codes Work

    Think of a dynamic QR code like a phone forwarding service. When someone calls your forwarding number (the short URL in the QR code), the service logs the call and forwards it to your real phone (the destination URL). You can change your real phone number anytime without changing the forwarding number people dial.

    -

    Example: A dynamic QR code might contain qrmaster.com/abc123. When scanned, this redirects to your actual URL: https://www.yourwebsite.com/summer-sale-2025. Later, you can change it to https://www.yourwebsite.com/fall-sale-2025 without reprinting.

    +

    Example: A dynamic QR code might contain qrmaster.net/abc123. When scanned, this redirects to your actual URL: https://www.yourwebsite.com/summer-sale-2025. Later, you can change it to https://www.yourwebsite.com/fall-sale-2025 without reprinting.

    Common Uses for Dynamic QR Codes

      @@ -769,7 +769,7 @@ No Tracking | Cannot Edit | Works Forever
    • ✅ Edit destination anytime: Change URL without reprinting QR codes—save thousands in reprint costs.
    • ✅ Full analytics: Track scans, geographic location, device types, time patterns, and user behavior.
    • ✅ A/B testing: Test different landing pages to optimize conversion rates.
    • -
    • ✅ Short, clean URLs: QR code contains qrmaster.com/abc123 instead of long ugly URLs.
    • +
    • ✅ Short, clean URLs: QR code contains qrmaster.net/abc123 instead of long ugly URLs.
    • ✅ Set expiration dates: Configure codes to stop working after campaigns end.
    • ✅ Password protection: Require password to access destination content.
    • ✅ Retargeting pixels: Add Facebook Pixel, Google Ads tracking for remarketing.
    • @@ -791,13 +791,13 @@ No Tracking | Cannot Edit | Works Forever

      Visual Example: Dynamic QR Code Data Flow

       Dynamic QR Code Content:
      -qrmaster.com/abc123
      +qrmaster.net/abc123
       
       User Scans QR Code
               ↓
       QR Scanner Decodes Pattern
               ↓
      -Contacts: qrmaster.com/abc123
      +Contacts: qrmaster.net/abc123
               ↓
       Server Logs: Device, Location, Time, User Agent
               ↓
      @@ -1197,7 +1197,7 @@ Will the destination URL ever change?
           image: '/blog/3-hero.png',
           imageAlt: 'Multiple QR codes arranged in organized grid pattern',
           author: 'QR Master Team',
      -    authorUrl: 'https://www.qrmaster.com/about',
      +    authorUrl: 'https://www.qrmaster.net/about',
           answer: 'Bulk QR code generation from Excel allows you to create hundreds or thousands of QR codes simultaneously by uploading a CSV or Excel file. The file should contain columns for name, URL, and optional metadata. Tools like QR Master Pro can process 1,000+ codes in minutes, saving hours of manual work. Perfect for product labels, event tickets, asset tracking, and marketing campaigns.',
           howTo: {
             name: 'How to Generate Bulk QR Codes from Excel',
      @@ -1211,7 +1211,7 @@ Will the destination URL ever change?
               {
                 name: 'Sign Up for QR Master Business',
                 text: 'Create a QR Master account and upgrade to Business plan for bulk upload feature (supports up to 500 codes).',
      -          url: 'https://www.qrmaster.com/signup',
      +          url: 'https://www.qrmaster.net/signup',
               },
               {
                 name: 'Upload Your File',
      @@ -1224,7 +1224,7 @@ Will the destination URL ever change?
               {
                 name: 'Generate and Download',
                 text: 'Click Generate All. Processing takes 2-4 minutes for 1,000 codes. Download the ZIP file with all QR codes organized by name.',
      -          url: 'https://www.qrmaster.com/bulk-qr-code-generator',
      +          url: 'https://www.qrmaster.net/bulk-qr-code-generator',
               },
             ],
           },
      @@ -1439,7 +1439,7 @@ Event Ticket 1 | https://event.com/ticket/1 | events, tickets
       
             

      Step 2: Sign Up for QR Master

        -
      1. Go to qrmaster.com/signup
      2. +
      3. Go to qrmaster.net/signup
      4. Create free account (email + password)
      5. Verify your email
      6. Free plan: Up to 3 dynamic QR codes (no bulk upload)
      7. @@ -1770,7 +1770,7 @@ Chicago-Store,https://promo.com?location=chicago,illinois retail

        For recurring bulk generation needs, use QR Master's API:

         // Example: Node.js API call
        -const response = await fetch('https://api.qrmaster.com/v1/bulk', {
        +const response = await fetch('https://api.qrmaster.net/v1/bulk', {
           method: 'POST',
           headers: {
             'Authorization': 'Bearer YOUR_API_KEY',
        @@ -1864,16 +1864,16 @@ export async function generateMetadata({ params }: { params: { slug: string } })
             title,
             description,
             alternates: {
        -      canonical: `https://www.qrmaster.com/blog/${params.slug}`,
        +      canonical: `https://www.qrmaster.net/blog/${params.slug}`,
               languages: {
        -        'x-default': `https://www.qrmaster.com/blog/${params.slug}`,
        -        en: `https://www.qrmaster.com/blog/${params.slug}`,
        +        'x-default': `https://www.qrmaster.net/blog/${params.slug}`,
        +        en: `https://www.qrmaster.net/blog/${params.slug}`,
               },
             },
             openGraph: {
               title,
               description,
        -      url: `https://www.qrmaster.com/blog/${params.slug}`,
        +      url: `https://www.qrmaster.net/blog/${params.slug}`,
               type: 'article',
               publishedTime: post.datePublished,
               modifiedTime: post.dateModified,
        diff --git a/src/app/(marketing)/blog/page.tsx b/src/app/(marketing)/blog/page.tsx
        index 7f480a3..99233ef 100644
        --- a/src/app/(marketing)/blog/page.tsx
        +++ b/src/app/(marketing)/blog/page.tsx
        @@ -26,16 +26,16 @@ export async function generateMetadata(): Promise {
             title,
             description,
             alternates: {
        -      canonical: 'https://www.qrmaster.com/blog',
        +      canonical: 'https://www.qrmaster.net/blog',
               languages: {
        -        'x-default': 'https://www.qrmaster.com/blog',
        -        en: 'https://www.qrmaster.com/blog',
        +        'x-default': 'https://www.qrmaster.net/blog',
        +        en: 'https://www.qrmaster.net/blog',
               },
             },
             openGraph: {
               title,
               description,
        -      url: 'https://www.qrmaster.com/blog',
        +      url: 'https://www.qrmaster.net/blog',
               type: 'website',
             },
             twitter: {
        diff --git a/src/app/(marketing)/bulk-qr-code-generator/page.tsx b/src/app/(marketing)/bulk-qr-code-generator/page.tsx
        index e363378..c5c43d4 100644
        --- a/src/app/(marketing)/bulk-qr-code-generator/page.tsx
        +++ b/src/app/(marketing)/bulk-qr-code-generator/page.tsx
        @@ -12,16 +12,16 @@ export const metadata: Metadata = {
           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',
        +    canonical: 'https://www.qrmaster.net/bulk-qr-code-generator',
             languages: {
        -      'x-default': 'https://www.qrmaster.com/bulk-qr-code-generator',
        -      en: 'https://www.qrmaster.com/bulk-qr-code-generator',
        +      'x-default': 'https://www.qrmaster.net/bulk-qr-code-generator',
        +      en: 'https://www.qrmaster.net/bulk-qr-code-generator',
             },
           },
           openGraph: {
             title: 'Bulk QR Code Generator - Create 1000s of QR Codes from Excel',
             description: 'Generate hundreds of QR codes at once from CSV or Excel files. Perfect for products, events, and inventory.',
        -    url: 'https://www.qrmaster.com/bulk-qr-code-generator',
        +    url: 'https://www.qrmaster.net/bulk-qr-code-generator',
             type: 'website',
           },
           twitter: {
        @@ -171,7 +171,7 @@ export default function BulkQRCodeGeneratorPage() {
           const softwareSchema = {
             '@context': 'https://schema.org',
             '@type': 'SoftwareApplication',
        -    '@id': 'https://www.qrmaster.com/bulk-qr-code-generator#software',
        +    '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#software',
             name: 'QR Master - Bulk QR Code Generator',
             applicationCategory: 'BusinessApplication',
             operatingSystem: 'Web Browser',
        @@ -202,7 +202,7 @@ export default function BulkQRCodeGeneratorPage() {
           const howToSchema = {
             '@context': 'https://schema.org',
             '@type': 'HowTo',
        -    '@id': 'https://www.qrmaster.com/bulk-qr-code-generator#howto',
        +    '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#howto',
             name: 'How to Generate Bulk QR Codes from Excel',
             description: 'Learn how to create hundreds of QR codes from an Excel or CSV file',
             totalTime: 'PT10M',
        @@ -218,7 +218,7 @@ export default function BulkQRCodeGeneratorPage() {
                 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',
        +        url: 'https://www.qrmaster.net/bulk-creation',
               },
               {
                 '@type': 'HowToStep',
        @@ -244,7 +244,7 @@ export default function BulkQRCodeGeneratorPage() {
           const faqSchema = {
             '@context': 'https://schema.org',
             '@type': 'FAQPage',
        -    '@id': 'https://www.qrmaster.com/bulk-qr-code-generator#faq',
        +    '@id': 'https://www.qrmaster.net/bulk-qr-code-generator#faq',
             mainEntity: [
               {
                 '@type': 'Question',
        diff --git a/src/app/(marketing)/dynamic-qr-code-generator/page.tsx b/src/app/(marketing)/dynamic-qr-code-generator/page.tsx
        index 06ac40e..0cb0a55 100644
        --- a/src/app/(marketing)/dynamic-qr-code-generator/page.tsx
        +++ b/src/app/(marketing)/dynamic-qr-code-generator/page.tsx
        @@ -12,16 +12,16 @@ export const metadata: Metadata = {
           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',
        +    canonical: 'https://www.qrmaster.net/dynamic-qr-code-generator',
             languages: {
        -      'x-default': 'https://www.qrmaster.com/dynamic-qr-code-generator',
        -      en: 'https://www.qrmaster.com/dynamic-qr-code-generator',
        +      'x-default': 'https://www.qrmaster.net/dynamic-qr-code-generator',
        +      en: 'https://www.qrmaster.net/dynamic-qr-code-generator',
             },
           },
           openGraph: {
             title: 'Dynamic QR Code Generator - Edit QR Codes Anytime | QR Master',
             description: 'Create dynamic QR codes that can be edited after printing. Change URLs, track scans, and update content anytime.',
        -    url: 'https://www.qrmaster.com/dynamic-qr-code-generator',
        +    url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
             type: 'website',
           },
           twitter: {
        @@ -132,7 +132,7 @@ export default function DynamicQRCodeGeneratorPage() {
           const softwareSchema = {
             '@context': 'https://schema.org',
             '@type': 'SoftwareApplication',
        -    '@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#software',
        +    '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#software',
             name: 'QR Master - Dynamic QR Code Generator',
             applicationCategory: 'BusinessApplication',
             operatingSystem: 'Web Browser',
        @@ -163,7 +163,7 @@ export default function DynamicQRCodeGeneratorPage() {
           const howToSchema = {
             '@context': 'https://schema.org',
             '@type': 'HowTo',
        -    '@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#howto',
        +    '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#howto',
             name: 'How to Create a Dynamic QR Code',
             description: 'Learn how to create editable QR codes that can be updated after printing',
             totalTime: 'PT3M',
        @@ -173,14 +173,14 @@ export default function DynamicQRCodeGeneratorPage() {
                 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',
        +        url: 'https://www.qrmaster.net/signup',
               },
               {
                 '@type': 'HowToStep',
                 position: 2,
                 name: 'Generate QR Code',
                 text: 'Enter your destination URL and customize the design with your branding',
        -        url: 'https://www.qrmaster.com/create',
        +        url: 'https://www.qrmaster.net/create',
               },
               {
                 '@type': 'HowToStep',
        @@ -193,7 +193,7 @@ export default function DynamicQRCodeGeneratorPage() {
                 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',
        +        url: 'https://www.qrmaster.net/dashboard',
               },
             ],
           };
        @@ -201,7 +201,7 @@ export default function DynamicQRCodeGeneratorPage() {
           const faqSchema = {
             '@context': 'https://schema.org',
             '@type': 'FAQPage',
        -    '@id': 'https://www.qrmaster.com/dynamic-qr-code-generator#faq',
        +    '@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#faq',
             mainEntity: [
               {
                 '@type': 'Question',
        diff --git a/src/app/(marketing)/faq/page.tsx b/src/app/(marketing)/faq/page.tsx
        index e3e6206..b69ac5d 100644
        --- a/src/app/(marketing)/faq/page.tsx
        +++ b/src/app/(marketing)/faq/page.tsx
        @@ -22,16 +22,16 @@ export async function generateMetadata(): Promise {
             title,
             description,
             alternates: {
        -      canonical: 'https://www.qrmaster.com/faq',
        +      canonical: 'https://www.qrmaster.net/faq',
               languages: {
        -        'x-default': 'https://www.qrmaster.com/faq',
        -        en: 'https://www.qrmaster.com/faq',
        +        'x-default': 'https://www.qrmaster.net/faq',
        +        en: 'https://www.qrmaster.net/faq',
               },
             },
             openGraph: {
               title,
               description,
        -      url: 'https://www.qrmaster.com/faq',
        +      url: 'https://www.qrmaster.net/faq',
               type: 'website',
             },
             twitter: {
        @@ -129,8 +129,8 @@ export default function FAQPage() {
                       
                       

        Our support team is here to help. Contact us at{' '} - - support@qrmaster.com + + support@qrmaster.net {' '} or reach out through our live chat.

        diff --git a/src/app/(marketing)/impressum/page.tsx b/src/app/(marketing)/impressum/page.tsx deleted file mode 100644 index 6218208..0000000 --- a/src/app/(marketing)/impressum/page.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; - -export const metadata = { - title: 'Legal Notice | QR Master', - description: 'Legal notice and company information for QR Master', -}; - -export default function ImpressumPage() { - return ( -
        -
        -
        - - ← Back to Home - -
        - -

        Legal Notice

        -

        Information according to § 5 TMG (German Telemedia Act)

        - -
        -
        -

        Service Provider

        -
        -

        Company Name: [Your Company / Your Name]

        -

        Legal Form: [e.g., Sole Proprietorship, LLC, Corporation]

        -

        Address:

        -

        - [Street and Number]
        - [Postal Code City]
        - [Country] -

        -
        -
        - -
        -

        Contact

        -
        -

        - Email:{' '} - - info@qrmaster.com - -

        -

        Phone: [Your Phone Number]

        -

        Website: www.qrmaster.com

        -
        -
        - -
        -

        Authorized Representative

        -
        -

        Managing Director / Owner: [Name]

        -
        -
        - -
        -

        Commercial Register

        -
        -

        Register Court: [e.g., Munich District Court]

        -

        Registration Number: [e.g., HRB 123456]

        -

        (If applicable)

        -
        -
        - -
        -

        VAT Identification Number

        -
        -

        - VAT ID according to § 27a UStG: [Your VAT ID] -

        -

        (If applicable)

        -
        -
        - -
        -

        Responsible for Content

        -

        - Responsible for content according to § 55 para. 2 RStV: -

        -
        -

        Name: [Name]

        -

        Address: [Same as above]

        -
        -
        - -
        -

        EU Dispute Resolution

        -

        - The European Commission provides a platform for online dispute resolution (ODR): - {' '} - https://ec.europa.eu/consumers/odr/ - -

        -

        - You can find our email address in the contact section above. -

        -
        - -
        -

        - Consumer Dispute Resolution / Universal Arbitration Board -

        -

        - We are not willing or obliged to participate in dispute resolution proceedings before a - consumer arbitration board. -

        -
        - -
        -

        Liability for Content

        -

        - As a service provider, we are responsible for our own content on these pages in accordance with § 7 para. 1 TMG - under general law. However, according to §§ 8 to 10 TMG, as a service provider we are not obligated to - monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity. -

        -

        - Obligations to remove or block the use of information according to general laws remain unaffected. However, - liability in this regard is only possible from the time of knowledge of a specific legal violation. Upon - becoming aware of corresponding legal violations, we will remove this content immediately. -

        -
        - -
        -

        Liability for Links

        -

        - Our website contains links to external third-party websites over whose content we have no influence. - Therefore, we cannot assume any liability for this external content. The respective provider or operator - of the pages is always responsible for the content of the linked pages. The linked pages were checked for - possible legal violations at the time of linking. Illegal content was not recognizable at the time of linking. -

        -

        - However, permanent monitoring of the content of linked pages is not reasonable without concrete evidence - of a legal violation. Upon becoming aware of legal violations, we will remove such links immediately. -

        -
        - -
        -

        Copyright

        -

        - The content and works created by the site operators on these pages are subject to German copyright law. - Duplication, processing, distribution, and any kind of exploitation outside the limits of copyright law - require the written consent of the respective author or creator. Downloads and copies of this site are - only permitted for private, non-commercial use. -

        -

        - Insofar as the content on this site was not created by the operator, the copyrights of third parties - are respected. In particular, third-party content is identified as such. Should you nevertheless become - aware of a copyright infringement, please inform us accordingly. Upon becoming aware of legal violations, - we will remove such content immediately. -

        -
        - -
        -

        Image Credits

        -

        - Images and graphics used on this website are from: -

        -
          -
        • Our own creation
        • -
        • License-free image sources (e.g., Unsplash, Pexels)
        • -
        • Licensed stock photo services
        • -
        -
        -
        - -
        -

        - - Privacy Policy - - - Terms of Service - - - Back to Home - -

        -
        -
        -
        - ); -} diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index 234d830..5b502f2 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import Link from 'next/link'; -import { usePathname } from 'next/navigation'; import { Button } from '@/components/ui/Button'; import en from '@/i18n/en.json'; @@ -11,7 +10,6 @@ export default function MarketingLayout({ }: { children: React.ReactNode; }) { - const pathname = usePathname(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); // Always use English for marketing pages @@ -142,10 +140,9 @@ export default function MarketingLayout({

        Legal

        • Privacy Policy
        • -
        • Terms of Service
        • -
        • Legal Notice
    +
    diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index 4d7221b..4094674 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -22,16 +22,16 @@ export async function generateMetadata(): Promise { title, description, alternates: { - canonical: 'https://www.qrmaster.com/', + canonical: 'https://www.qrmaster.net/', languages: { - 'x-default': 'https://www.qrmaster.com/', - en: 'https://www.qrmaster.com/', + 'x-default': 'https://www.qrmaster.net/', + en: 'https://www.qrmaster.net/', }, }, openGraph: { title, description, - url: 'https://www.qrmaster.com/', + url: 'https://www.qrmaster.net/', type: 'website', }, twitter: { diff --git a/src/app/(marketing)/privacy/page.tsx b/src/app/(marketing)/privacy/page.tsx index 28c870d..afe1ba6 100644 --- a/src/app/(marketing)/privacy/page.tsx +++ b/src/app/(marketing)/privacy/page.tsx @@ -24,199 +24,104 @@ export default function PrivacyPage() {

    1. Introduction

    Welcome to QR Master ("we," "our," or "us"). We respect your privacy and are committed to protecting your personal data. - This privacy policy will inform you about how we look after your personal data when you visit our website and use our services, - and tell you about your privacy rights and how the law protects you. + This privacy policy explains how we collect, use, and protect your information when you use our services. +

    +

    + We implement appropriate security measures including secure HTTPS transmission, password hashing, database access controls, + and CSRF protection to keep your data safe.

    2. Information We Collect

    -

    We collect and process the following data about you:

    -

    2.1 Information You Provide

    +

    Information You Provide

      -
    • Account Information: Name, email address, and password when you create an account
    • -
    • Payment Information: Payment details processed securely through Stripe (we do not store credit card information)
    • -
    • QR Code Data: Content, URLs, and customization settings for QR codes you create
    • -
    • Profile Information: Any additional information you choose to provide
    • +
    • Account Information: Name, email address, and password
    • +
    • Payment Information: Processed securely through Stripe (we do not store credit card information)
    • +
    • QR Code Content: URLs, text, and customization settings for your QR codes
    -

    2.2 Information We Collect Automatically

    +

    Information Collected Automatically

      -
    • Usage Data: QR code scans, analytics data, and interaction with our services
    • -
    • Technical Data: IP address, browser type, device information, and operating system
    • -
    • Analytics Data: Website usage analytics collected via PostHog (only with your consent)
    • -
    • Cookies: We use cookies to improve your experience (see our Cookie Policy below)
    • +
    • Usage Data: QR code scans and analytics
    • +
    • Technical Data: IP address, browser type, and device information
    • +
    • Cookies: Essential cookies for authentication and optional analytics cookies (PostHog) with your consent

    3. How We Use Your Information

    -

    We use your personal data for the following purposes:

    +

    We use your data to:

      -
    • To provide and maintain our QR code generation and analytics services
    • -
    • To process your payments and manage your subscription
    • -
    • To provide customer support and respond to your inquiries
    • -
    • To improve our services and develop new features
    • -
    • To detect and prevent fraud and abuse
    • -
    • To comply with legal obligations
    • -
    -
    - -
    -

    4. Legal Basis for Processing (GDPR)

    -

    We process your personal data under the following legal bases:

    -
      -
    • Contract Performance: Processing necessary to provide our services to you
    • -
    • Consent: Where you have given clear consent for specific purposes
    • -
    • Legitimate Interests: For improving our services, security, and fraud prevention
    • -
    • Legal Obligation: To comply with applicable laws and regulations
    • -
    -
    - -
    -

    5. Data Sharing and Third Parties

    -

    We may share your data with the following third parties:

    -
      -
    • Stripe: Payment processing (subject to Stripe's privacy policy)
    • -
    • PostHog: Website analytics platform for tracking user behavior and improving our services (only with your consent, subject to PostHog's privacy policy)
    • -
    • Cloud Hosting: Vercel and database providers for hosting our services
    • -
    • Service Providers: Companies that help us provide our services (under strict confidentiality agreements)
    • -
    • Legal Requirements: When required by law or to protect our rights
    • +
    • Provide and maintain our QR code services
    • +
    • Process payments and manage subscriptions
    • +
    • Provide customer support
    • +
    • Improve our services and develop new features
    • +
    • Detect and prevent fraud

    - We do not sell your personal data to third parties. PostHog analytics are only activated if you accept analytics cookies, - and we use privacy-friendly settings including respecting Do Not Track (DNT) headers. + We retain your data while your account is active. Upon account deletion, most data is removed immediately, + though some may be retained for legal compliance. Aggregated, anonymized analytics may be kept indefinitely.

    -

    6. Data Security

    -

    - We implement appropriate technical and organizational measures to protect your personal data, including: -

    +

    4. Data Sharing

    +

    We may share your data with:

      -
    • Secure HTTPS transmission for data in transit
    • -
    • Secure password hashing using bcrypt
    • -
    • Database security and access controls
    • -
    • Cookie-based authentication with HttpOnly flags
    • -
    • CSRF protection for sensitive operations
    • -
    • Rate limiting to prevent abuse
    • +
    • Stripe: Payment processing
    • +
    • PostHog: Analytics (only with your consent, respects Do Not Track)
    • +
    • Vercel: Cloud hosting provider
    • +
    • Legal Requirements: When required by law
    +

    + We do not sell your personal data. Analytics are only activated if you accept optional cookies. +

    -

    7. Data Retention

    -

    - We retain your personal data only for as long as necessary to fulfill the purposes outlined in this policy: -

    +

    5. Your Rights (GDPR)

    +

    You have the right to:

      -
    • Active Accounts: Data retained while your account is active
    • -
    • Deleted Accounts: Most data deleted immediately upon account deletion
    • -
    • Legal Requirements: Some data may be retained to comply with legal obligations
    • -
    • Analytics Data: Aggregated, anonymized data may be retained indefinitely
    • -
    -
    - -
    -

    8. Your Rights (GDPR)

    -

    Under GDPR, you have the following rights:

    -
      -
    • Right to Access: Request a copy of your personal data
    • -
    • Right to Rectification: Correct inaccurate or incomplete data (edit name in settings)
    • -
    • Right to Erasure: Request deletion of your data (account deletion available in settings)
    • -
    • Right to Restriction: Request limitation of processing
    • -
    • Right to Data Portability: Receive your data in a portable format (available upon request)
    • -
    • Right to Object: Object to processing based on legitimate interests
    • -
    • Right to Withdraw Consent: Withdraw consent at any time
    • +
    • Access: Request a copy of your personal data
    • +
    • Rectification: Correct inaccurate data (update in account settings)
    • +
    • Erasure: Delete your data (account deletion available in settings)
    • +
    • Data Portability: Receive your data in a portable format
    • +
    • Object: Object to processing based on legitimate interests
    • +
    • Withdraw Consent: Withdraw cookie consent at any time

    - To exercise these rights, please contact us at{' '} - - privacy@qrmaster.com + To exercise these rights, contact us at{' '} + + support@qrmaster.net

    -
    - -
    -

    9. Cookies

    - We use cookies to improve your experience on our website. Cookies are small text files stored on your device. -

    -

    Types of Cookies We Use:

    -
      -
    • Essential Cookies: Required for authentication and basic functionality (userId, CSRF token)
    • -
    • Preference Cookies: Remember your settings and cookie consent preferences (cookieConsent)
    • -
    • Analytics Cookies: PostHog analytics cookies to track page views, user behavior, and improve our services (only with your consent)
    • -
    -

    - You can control cookies through your browser settings and our cookie banner. Analytics cookies are only set if you accept them - through our cookie banner. Essential cookies are required for the website to function and cannot be disabled. -

    -

    PostHog Analytics:

    -

    - PostHog is our analytics platform that helps us understand how users interact with our website. When you accept analytics cookies: -

    -
      -
    • PostHog tracks page views, clicks, and user journeys
    • -
    • We collect device type, browser, operating system, and referral source
    • -
    • PostHog respects Do Not Track (DNT) browser settings
    • -
    • No personally identifiable information (PII) is sent to PostHog without explicit identification
    • -
    • Data is processed in accordance with PostHog's privacy policy
    • -
    -
    - -
    -

    10. International Data Transfers

    -

    - Your data may be transferred to and processed in countries outside the European Economic Area (EEA). - We ensure appropriate safeguards are in place, including Standard Contractual Clauses (SCCs) and - adequacy decisions by the European Commission. + Our service is for users 16 years and older. If you're in the EEA and have concerns, + you may lodge a complaint with your local data protection authority.

    -

    11. Children's Privacy

    +

    6. Contact Us

    - Our services are not intended for children under 16 years of age. We do not knowingly collect personal data - from children. If you believe we have collected data from a child, please contact us immediately. -

    -
    - -
    -

    12. Changes to This Policy

    -

    - We may update this privacy policy from time to time. We will notify you of significant changes through - a prominent notice on our website. Continued use of our services after changes constitutes - acceptance of the updated policy. -

    -
    - -
    -

    13. Contact Us

    -

    - If you have any questions about this privacy policy or our data practices, please contact us: + If you have questions about this privacy policy, please contact us:

    -

    Email: privacy@qrmaster.com

    -

    Website: qrmaster.com

    +

    + Email:{' '} + + support@qrmaster.net + +

    +

    Website: qrmaster.net

    - -
    -

    14. Supervisory Authority

    -

    - If you are located in the EEA and believe we have not addressed your concerns adequately, - you have the right to lodge a complaint with your local data protection supervisory authority. -

    -

    - - Terms of Service - Back to Home diff --git a/src/app/(marketing)/qr-code-tracking/page.tsx b/src/app/(marketing)/qr-code-tracking/page.tsx index f3f8c52..32e2717 100644 --- a/src/app/(marketing)/qr-code-tracking/page.tsx +++ b/src/app/(marketing)/qr-code-tracking/page.tsx @@ -12,16 +12,16 @@ export const metadata: Metadata = { 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', + canonical: 'https://www.qrmaster.net/qr-code-tracking', languages: { - 'x-default': 'https://www.qrmaster.com/qr-code-tracking', - en: 'https://www.qrmaster.com/qr-code-tracking', + 'x-default': 'https://www.qrmaster.net/qr-code-tracking', + en: 'https://www.qrmaster.net/qr-code-tracking', }, }, openGraph: { title: 'QR Code Tracking & Analytics - Track Every Scan | QR Master', description: 'Track QR code scans with real-time analytics. Monitor location, device, time, and user behavior.', - url: 'https://www.qrmaster.com/qr-code-tracking', + url: 'https://www.qrmaster.net/qr-code-tracking', type: 'website', }, twitter: { @@ -100,7 +100,7 @@ export default function QRCodeTrackingPage() { const softwareSchema = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', - '@id': 'https://www.qrmaster.com/qr-code-tracking#software', + '@id': 'https://www.qrmaster.net/qr-code-tracking#software', name: 'QR Master - QR Code Tracking & Analytics', applicationCategory: 'BusinessApplication', operatingSystem: 'Web Browser, iOS, Android', @@ -131,7 +131,7 @@ export default function QRCodeTrackingPage() { const howToSchema = { '@context': 'https://schema.org', '@type': 'HowTo', - '@id': 'https://www.qrmaster.com/qr-code-tracking#howto', + '@id': 'https://www.qrmaster.net/qr-code-tracking#howto', name: 'How to Track QR Code Scans', description: 'Learn how to track and analyze QR code scans with real-time analytics', totalTime: 'PT5M', @@ -141,7 +141,7 @@ export default function QRCodeTrackingPage() { 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', + url: 'https://www.qrmaster.net/signup', }, { '@type': 'HowToStep', @@ -154,7 +154,7 @@ export default function QRCodeTrackingPage() { 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', + url: 'https://www.qrmaster.net/analytics', }, { '@type': 'HowToStep', diff --git a/src/app/(marketing)/terms/page.tsx b/src/app/(marketing)/terms/page.tsx deleted file mode 100644 index 1c15dc2..0000000 --- a/src/app/(marketing)/terms/page.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; - -export const metadata = { - title: 'Terms of Service | QR Master', - description: 'Terms of Service and usage conditions for QR Master', -}; - -export default function TermsPage() { - return ( -

    -
    -
    - - ← Back to Home - -
    - -

    Terms of Service

    -

    Last updated: January 2025

    - -
    -
    -

    1. Agreement to Terms

    -

    - By accessing or using QR Master ("Service"), you agree to be bound by these Terms of Service ("Terms"). - If you disagree with any part of these terms, you may not access the Service. -

    -

    - These Terms apply to all visitors, users, and others who access or use the Service. -

    -
    - -
    -

    2. Description of Service

    -

    - QR Master is a SaaS platform that provides: -

    -
      -
    • QR code generation (static and dynamic)
    • -
    • QR code customization and branding
    • -
    • Analytics and tracking for dynamic QR codes
    • -
    • Bulk QR code creation
    • -
    • QR code management dashboard
    • -
    -
    - -
    -

    3. Account Registration

    - -

    3.1 Account Creation

    -

    - To use certain features, you must create an account. You agree to: -

    -
      -
    • Provide accurate, current, and complete information
    • -
    • Maintain and update your information to keep it accurate
    • -
    • Maintain the security of your password
    • -
    • Accept responsibility for all activities under your account
    • -
    • Notify us immediately of any unauthorized use
    • -
    - -

    3.2 Account Eligibility

    -

    - You must be at least 16 years old to use this Service. By creating an account, you represent that you meet this requirement. -

    - -

    3.3 Account Termination

    -

    - We reserve the right to suspend or terminate your account at any time for violations of these Terms, - fraudulent activity, or other reasons at our sole discretion. -

    -
    - -
    -

    4. Subscription Plans and Payments

    - -

    4.1 Plans

    -

    - We offer multiple subscription tiers: -

    -
      -
    • Free Plan: Limited features with usage restrictions
    • -
    • Pro Plan: Enhanced features and higher limits
    • -
    • Business Plan: Full features with maximum limits
    • -
    - -

    4.2 Billing

    -

    - Paid subscriptions are billed monthly in advance. By subscribing, you agree to: -

    -
      -
    • Pay all fees associated with your chosen plan
    • -
    • Provide current, complete, and accurate billing information
    • -
    • Update payment information promptly to avoid service interruption
    • -
    • Accept that fees are non-refundable except as required by law
    • -
    - -

    4.3 Payment Processing

    -

    - All payments are processed securely through Stripe. We do not store your credit card information. - Your payment information is subject to Stripe's Terms of Service and Privacy Policy. -

    - -

    4.4 Automatic Renewal

    -

    - Subscriptions automatically renew at the end of each billing period unless cancelled. - You may cancel your subscription at any time through your account settings or the Stripe Customer Portal. -

    - -

    4.5 Cancellation and Refunds

    -

    - You may cancel your subscription at any time. Cancellation takes effect at the end of the current billing period. - No refunds will be provided for partial months or unused services, except as required by applicable law. -

    - -

    4.6 Price Changes

    -

    - We reserve the right to change our pricing. We will provide at least 30 days' notice before any price changes take effect. - Continued use of the Service after a price change constitutes acceptance of the new pricing. -

    -
    - -
    -

    5. Acceptable Use

    - -

    You agree NOT to use the Service to:

    -
      -
    • Violate any laws or regulations
    • -
    • Infringe on intellectual property rights
    • -
    • Transmit malware, viruses, or harmful code
    • -
    • Engage in phishing, spam, or fraudulent activities
    • -
    • Create QR codes linking to illegal, harmful, or malicious content
    • -
    • Harass, abuse, or harm others
    • -
    • Impersonate any person or entity
    • -
    • Interfere with or disrupt the Service
    • -
    • Attempt to gain unauthorized access to our systems
    • -
    • Use automated tools to access the Service without permission
    • -
    • Resell or redistribute the Service without authorization
    • -
    - -

    - We reserve the right to investigate and take appropriate legal action against anyone who violates these provisions. -

    -
    - -
    -

    6. Content and Intellectual Property

    - -

    6.1 Your Content

    -

    - You retain all rights to the content you create using our Service (URLs, text, images in QR codes, etc.). - By using our Service, you grant us a limited license to store, process, and display your content solely - for the purpose of providing the Service to you. -

    - -

    6.2 Our Content

    -

    - The Service and its original content (excluding user-generated content), features, and functionality - are owned by QR Master and are protected by international copyright, trademark, and other intellectual property laws. -

    - -

    6.3 Generated QR Codes

    -

    - QR codes you generate using our Service are yours to use as you wish. However, you are responsible for ensuring - that the content encoded in QR codes complies with these Terms and applicable laws. -

    -
    - -
    -

    7. Service Availability and Modifications

    - -

    7.1 Service Availability

    -

    - We strive to provide reliable service but do not guarantee uninterrupted access. The Service may be temporarily - unavailable due to maintenance, updates, or circumstances beyond our control. -

    - -

    7.2 Modifications

    -

    - We reserve the right to modify, suspend, or discontinue any part of the Service at any time with or without notice. - We will not be liable to you or any third party for any modification, suspension, or discontinuation of the Service. -

    -
    - -
    -

    8. Data and Privacy

    -

    - Your use of the Service is also governed by our Privacy Policy. Please review our{' '} - Privacy Policy - {' '}to understand how we collect, use, and protect your data. -

    -
    - -
    -

    9. Disclaimers and Limitations of Liability

    - -

    9.1 Disclaimer of Warranties

    -

    - THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. -

    - -

    9.2 Limitation of Liability

    -

    - TO THE MAXIMUM EXTENT PERMITTED BY LAW, QR MASTER SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, - CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES, WHETHER INCURRED DIRECTLY OR INDIRECTLY, - OR ANY LOSS OF DATA, USE, GOODWILL, OR OTHER INTANGIBLE LOSSES. -

    - -

    9.3 Maximum Liability

    -

    - Our total liability to you for all claims arising from your use of the Service shall not exceed the amount - you paid us in the 12 months preceding the claim. -

    -
    - -
    -

    10. Indemnification

    -

    - You agree to indemnify and hold harmless QR Master and its officers, directors, employees, and agents from any - claims, damages, losses, liabilities, and expenses (including legal fees) arising from: -

    -
      -
    • Your use of the Service
    • -
    • Your violation of these Terms
    • -
    • Your violation of any rights of another party
    • -
    • Content you create or share using the Service
    • -
    -
    - -
    -

    11. Governing Law and Dispute Resolution

    - -

    11.1 Governing Law

    -

    - These Terms shall be governed by and construed in accordance with the laws of the jurisdiction in which - QR Master operates, without regard to conflict of law provisions. -

    - -

    11.2 Dispute Resolution

    -

    - Any disputes arising from these Terms or your use of the Service shall first be attempted to be resolved - through good-faith negotiation. If unresolved, disputes may be brought in the courts of competent jurisdiction. -

    -
    - -
    -

    12. Changes to Terms

    -

    - We reserve the right to modify these Terms at any time. We will provide notice of significant changes by: -

    -
      -
    • Posting the updated Terms with a new "Last Updated" date
    • -
    • Displaying a prominent notice on our website
    • -
    -

    - Your continued use of the Service after changes take effect constitutes acceptance of the revised Terms. - If you do not agree to the new Terms, you must stop using the Service and may cancel your account. -

    -
    - -
    -

    13. Severability

    -

    - If any provision of these Terms is found to be unenforceable or invalid, that provision will be limited - or eliminated to the minimum extent necessary, and the remaining provisions will remain in full force and effect. -

    -
    - -
    -

    14. Entire Agreement

    -

    - These Terms, together with our Privacy Policy, constitute the entire agreement between you and QR Master - regarding the Service and supersede all prior agreements and understandings. -

    -
    - -
    -

    15. Contact Information

    -

    - If you have questions about these Terms, please contact us: -

    -
    -

    Email: legal@qrmaster.com

    -

    Website: qrmaster.com

    -
    -
    -
    - -
    -

    - - Privacy Policy - - - Back to Home - -

    -
    -
    -
    - ); -} diff --git a/src/app/api/analytics/summary/route.ts b/src/app/api/analytics/summary/route.ts index cedd2c2..9233565 100644 --- a/src/app/api/analytics/summary/route.ts +++ b/src/app/api/analytics/summary/route.ts @@ -5,6 +5,20 @@ import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; export const dynamic = 'force-dynamic'; +// Helper function to calculate trend +function calculateTrend(current: number, previous: number): { trend: 'up' | 'down' | 'flat'; percentage: number } { + if (previous === 0) { + return current > 0 ? { trend: 'up', percentage: 100 } : { trend: 'flat', percentage: 0 }; + } + + const change = ((current - previous) / previous) * 100; + const percentage = Math.round(Math.abs(change)); + + if (change > 5) return { trend: 'up', percentage }; + if (change < -5) return { trend: 'down', percentage }; + return { trend: 'flat', percentage }; +} + export async function GET(request: NextRequest) { try { const userId = cookies().get('userId')?.value; @@ -33,17 +47,58 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - // Get user's QR codes + // Get date range from query params (default: last 30 days) + const { searchParams } = request.nextUrl; + const range = searchParams.get('range') || '30'; + const daysInRange = parseInt(range, 10); + + // Calculate current and previous period dates + const now = new Date(); + const currentPeriodStart = new Date(); + currentPeriodStart.setDate(now.getDate() - daysInRange); + + const previousPeriodEnd = new Date(currentPeriodStart); + const previousPeriodStart = new Date(previousPeriodEnd); + previousPeriodStart.setDate(previousPeriodEnd.getDate() - daysInRange); + + // Get user's QR codes with scans filtered by period const qrCodes = await db.qRCode.findMany({ where: { userId }, include: { - scans: true, + scans: { + where: { + ts: { + gte: currentPeriodStart, + }, + }, + }, }, }); - // Calculate stats + // Get previous period scans for comparison + const qrCodesWithPreviousScans = await db.qRCode.findMany({ + where: { userId }, + include: { + scans: { + where: { + ts: { + gte: previousPeriodStart, + lt: previousPeriodEnd, + }, + }, + }, + }, + }); + + // Calculate current period stats const totalScans = qrCodes.reduce((sum, qr) => sum + qr.scans.length, 0); - const uniqueScans = qrCodes.reduce((sum, qr) => + const uniqueScans = qrCodes.reduce((sum, qr) => + sum + qr.scans.filter(s => s.isUnique).length, 0 + ); + + // Calculate previous period stats for comparison + const previousTotalScans = qrCodesWithPreviousScans.reduce((sum, qr) => sum + qr.scans.length, 0); + const previousUniqueScans = qrCodesWithPreviousScans.reduce((sum, qr) => sum + qr.scans.filter(s => s.isUnique).length, 0 ); @@ -60,44 +115,59 @@ export async function GET(request: NextRequest) { ? Math.round((mobileScans / totalScans) * 100) : 0; - // Country stats + // Country stats (current period) const countryStats = qrCodes.flatMap(qr => qr.scans) .reduce((acc, scan) => { const country = scan.country || 'Unknown'; acc[country] = (acc[country] || 0) + 1; return acc; }, {} as Record); - + + // Country stats (previous period) + const previousCountryStats = qrCodesWithPreviousScans.flatMap(qr => qr.scans) + .reduce((acc, scan) => { + const country = scan.country || 'Unknown'; + acc[country] = (acc[country] || 0) + 1; + return acc; + }, {} as Record); + const topCountry = Object.entries(countryStats) .sort(([,a], [,b]) => b - a)[0]; - // Time-based stats (last 30 days) - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - - const recentScans = qrCodes.flatMap(qr => qr.scans) - .filter(scan => new Date(scan.ts) > thirtyDaysAgo); - - // Daily scan counts for chart - const dailyScans = recentScans.reduce((acc, scan) => { + // Daily scan counts for chart (current period) + const dailyScans = qrCodes.flatMap(qr => qr.scans).reduce((acc, scan) => { const date = new Date(scan.ts).toISOString().split('T')[0]; acc[date] = (acc[date] || 0) + 1; return acc; }, {} as Record); - + // QR performance (only show DYNAMIC QR codes since STATIC don't track scans) const qrPerformance = qrCodes .filter(qr => qr.type === 'DYNAMIC') - .map(qr => ({ - id: qr.id, - title: qr.title, - type: qr.type, - totalScans: qr.scans.length, - uniqueScans: qr.scans.filter(s => s.isUnique).length, - conversion: qr.scans.length > 0 - ? Math.round((qr.scans.filter(s => s.isUnique).length / qr.scans.length) * 100) - : 0, - })) + .map(qr => { + const currentTotal = qr.scans.length; + const currentUnique = qr.scans.filter(s => s.isUnique).length; + + // Find previous period data for this QR code + const previousQR = qrCodesWithPreviousScans.find(prev => prev.id === qr.id); + const previousTotal = previousQR ? previousQR.scans.length : 0; + + // Calculate trend + const trendData = calculateTrend(currentTotal, previousTotal); + + return { + id: qr.id, + title: qr.title, + type: qr.type, + totalScans: currentTotal, + uniqueScans: currentUnique, + conversion: currentTotal > 0 + ? Math.round((currentUnique / currentTotal) * 100) + : 0, + trend: trendData.trend, + trendPercentage: trendData.percentage, + }; + }) .sort((a, b) => b.totalScans - a.totalScans); return NextResponse.json({ @@ -117,13 +187,20 @@ export async function GET(request: NextRequest) { countryStats: Object.entries(countryStats) .sort(([,a], [,b]) => b - a) .slice(0, 10) - .map(([country, count]) => ({ - country, - count, - percentage: totalScans > 0 - ? Math.round((count / totalScans) * 100) - : 0, - })), + .map(([country, count]) => { + const previousCount = previousCountryStats[country] || 0; + const trendData = calculateTrend(count, previousCount); + + return { + country, + count, + percentage: totalScans > 0 + ? Math.round((count / totalScans) * 100) + : 0, + trend: trendData.trend, + trendPercentage: trendData.percentage, + }; + }), dailyScans, qrPerformance: qrPerformance.slice(0, 10), }); diff --git a/src/app/api/qrs/[id]/route.ts b/src/app/api/qrs/[id]/route.ts index fe64a69..4bf0c72 100644 --- a/src/app/api/qrs/[id]/route.ts +++ b/src/app/api/qrs/[id]/route.ts @@ -10,7 +10,6 @@ const updateQRSchema = z.object({ content: z.any().optional(), tags: z.array(z.string()).optional(), style: z.any().optional(), - status: z.enum(['ACTIVE', 'PAUSED']).optional(), }); // GET /api/qrs/[id] - Get a single QR code @@ -120,7 +119,6 @@ export async function PATCH( ...(data.content && { content: data.content }), ...(data.tags && { tags: data.tags }), ...(data.style && { style: data.style }), - ...(data.status && { status: data.status }), }, }); diff --git a/src/app/api/user/plan/route.ts b/src/app/api/user/plan/route.ts index 8c83b77..caf3a0b 100644 --- a/src/app/api/user/plan/route.ts +++ b/src/app/api/user/plan/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { db } from '@/lib/db'; +import { STRIPE_PLANS } from '@/lib/stripe'; export const dynamic = 'force-dynamic'; @@ -28,8 +29,21 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } + // Determine billing interval from stripePriceId + let interval: 'month' | 'year' | null = null; + + if (user.stripePriceId) { + // Check if the current price ID matches any yearly price ID + const isYearly = + user.stripePriceId === STRIPE_PLANS.PRO.priceIdYearly || + user.stripePriceId === STRIPE_PLANS.BUSINESS.priceIdYearly; + + interval = isYearly ? 'year' : 'month'; + } + return NextResponse.json({ plan: user.plan || 'FREE', + interval, currentPeriodEnd: user.stripeCurrentPeriodEnd, priceId: user.stripePriceId, stripeCustomerId: user.stripeCustomerId, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fe10bc4..f2797b0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,7 +8,7 @@ import CookieBanner from '@/components/CookieBanner'; const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true'; export const metadata: Metadata = { - metadataBase: new URL('https://www.qrmaster.com'), + metadataBase: new URL('https://www.qrmaster.net'), title: { default: 'QR Master – Smart QR Generator & Analytics', template: '%s | QR Master', @@ -28,17 +28,17 @@ export const metadata: Metadata = { twitter: { card: 'summary_large_image', site: '@qrmaster', - images: ['https://www.qrmaster.com/static/og-image.png'], + images: ['https://www.qrmaster.net/static/og-image.png'], }, openGraph: { type: 'website', siteName: 'QR Master', title: 'QR Master – Smart QR Generator & Analytics', description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.', - url: 'https://www.qrmaster.com', + url: 'https://www.qrmaster.net', images: [ { - url: 'https://www.qrmaster.com/static/og-image.png', + url: 'https://www.qrmaster.net/static/og-image.png', width: 1200, height: 630, alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform', diff --git a/src/app/r/[slug]/route.ts b/src/app/r/[slug]/route.ts index a65a4b5..1a2c119 100644 --- a/src/app/r/[slug]/route.ts +++ b/src/app/r/[slug]/route.ts @@ -14,7 +14,6 @@ export async function GET( where: { slug }, select: { id: true, - status: true, content: true, contentType: true, }, @@ -24,10 +23,6 @@ export async function GET( return new NextResponse('QR Code not found', { status: 404 }); } - if (qrCode.status === 'PAUSED') { - return new NextResponse('QR Code is paused', { status: 404 }); - } - // Track scan (fire and forget) trackScan(qrCode.id, request).catch(console.error); diff --git a/src/app/robots.ts b/src/app/robots.ts index cc3f113..2906c28 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,7 +1,7 @@ import { MetadataRoute } from 'next'; export default function robots(): MetadataRoute.Robots { - const baseUrl = 'https://www.qrmaster.com'; + const baseUrl = 'https://www.qrmaster.net'; return { rules: [ diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 4a9c54c..e05f66e 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,7 +1,7 @@ import { MetadataRoute } from 'next'; export default function sitemap(): MetadataRoute.Sitemap { - const baseUrl = 'https://www.qrmaster.com'; + const baseUrl = 'https://www.qrmaster.net'; return [ { diff --git a/src/components/dashboard/QRCodeCard.tsx b/src/components/dashboard/QRCodeCard.tsx index 16a3782..bf385bd 100644 --- a/src/components/dashboard/QRCodeCard.tsx +++ b/src/components/dashboard/QRCodeCard.tsx @@ -15,20 +15,17 @@ interface QRCodeCardProps { contentType: string; content?: any; slug: string; - status: 'ACTIVE' | 'PAUSED'; createdAt: string; scans?: number; style?: any; }; onEdit: (id: string) => void; - onPause: (id: string) => void; onDelete: (id: string) => void; } export const QRCodeCard: React.FC = ({ qr, onEdit, - onPause, onDelete, }) => { // For dynamic QR codes, use the redirect URL for tracking @@ -172,9 +169,6 @@ END:VCARD`; {qr.type} - - {qr.status} -
    @@ -193,11 +187,6 @@ END:VCARD`; {qr.type === 'DYNAMIC' && ( onEdit(qr.id)}>Edit )} - {qr.type === 'DYNAMIC' && ( - onPause(qr.id)}> - {qr.status === 'ACTIVE' ? 'Pause' : 'Resume'} - - )} onDelete(qr.id)} className="text-red-600"> Delete diff --git a/src/components/marketing/Hero.tsx b/src/components/marketing/Hero.tsx index c9ebfef..b67c62a 100644 --- a/src/components/marketing/Hero.tsx +++ b/src/components/marketing/Hero.tsx @@ -20,7 +20,22 @@ export const Hero: React.FC = ({ t }) => { return (
    -
    + {/* Animated Background Orbs */} +
    + {/* Orb 1 - Blue (top-left) */} +
    + + {/* Orb 2 - Purple (top-right) */} +
    + + {/* Orb 3 - Pink (bottom-left) */} +
    + + {/* Orb 4 - Cyan (center-right) */} +
    +
    + +
    {/* Left Content */}
    @@ -83,6 +98,9 @@ export const Hero: React.FC = ({ t }) => {
    + + {/* Smooth Gradient Fade Transition */} +
    ); }; \ No newline at end of file diff --git a/src/components/marketing/Pricing.tsx b/src/components/marketing/Pricing.tsx index b5f3282..2bb2632 100644 --- a/src/components/marketing/Pricing.tsx +++ b/src/components/marketing/Pricing.tsx @@ -1,16 +1,19 @@ 'use client'; -import React from 'react'; +import React, { useState } 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'; +import { BillingToggle } from '@/components/ui/BillingToggle'; interface PricingProps { t: any; // i18n translation function } export const Pricing: React.FC = ({ t }) => { + const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); + const plans = [ { key: 'free', @@ -38,6 +41,10 @@ export const Pricing: React.FC = ({ t }) => {

    +
    + +
    +
    {plans.map((plan) => ( = ({ t }) => { {t.pricing[plan.key].title} -
    - - {t.pricing[plan.key].price} - - - {t.pricing[plan.key].period} - +
    +
    + + {plan.key === 'free' + ? t.pricing[plan.key].price + : billingPeriod === 'month' + ? t.pricing[plan.key].price + : plan.key === 'pro' + ? '€90' + : '€290'} + + + {plan.key === 'free' + ? t.pricing[plan.key].period + : billingPeriod === 'month' + ? t.pricing[plan.key].period + : 'per year'} + +
    + {billingPeriod === 'year' && plan.key !== 'free' && ( + + Save 16% + + )}
    diff --git a/src/components/ui/BillingToggle.tsx b/src/components/ui/BillingToggle.tsx new file mode 100644 index 0000000..3957543 --- /dev/null +++ b/src/components/ui/BillingToggle.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface BillingToggleProps { + value: 'month' | 'year'; + onChange: (value: 'month' | 'year') => void; +} + +export const BillingToggle: React.FC = ({ value, onChange }) => { + return ( +
    + + +
    + ); +}; diff --git a/src/i18n/en.json b/src/i18n/en.json index 05e41c7..4f6df67 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -135,7 +135,8 @@ "3 dynamic QR codes", "Unlimited static QR codes", "Basic scan tracking", - "Standard QR design templates" + "Standard QR design templates", + "Download as SVG/PNG" ] }, "pro": { @@ -148,8 +149,7 @@ "50 dynamic QR codes", "Unlimited static QR codes", "Advanced analytics (scans, devices, locations)", - "Custom branding (colors)", - "Download as SVG/PNG" + "Custom branding (colors)" ] }, "business": { diff --git a/src/lib/email.ts b/src/lib/email.ts index ec3657a..2e3705a 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -9,6 +9,7 @@ export async function sendPasswordResetEmail(email: string, resetToken: string) try { await resend.emails.send({ from: 'QR Master ', // Use Resend's testing domain + replyTo: 'support@qrmaster.net', to: email, subject: 'Reset Your Password - QR Master', html: ` diff --git a/src/lib/schema.ts b/src/lib/schema.ts index d302040..fc84c56 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -44,12 +44,12 @@ export function organizationSchema() { return { '@context': 'https://schema.org', '@type': 'Organization', - '@id': 'https://www.qrmaster.com/#organization', + '@id': 'https://www.qrmaster.net/#organization', name: 'QR Master', - url: 'https://www.qrmaster.com', + url: 'https://www.qrmaster.net', logo: { '@type': 'ImageObject', - url: 'https://www.qrmaster.com/static/og-image.png', + url: 'https://www.qrmaster.net/static/og-image.png', width: 1200, height: 630, }, @@ -59,11 +59,11 @@ export function organizationSchema() { contactPoint: { '@type': 'ContactPoint', contactType: 'Customer Support', - email: 'support@qrmaster.com', + email: 'support@qrmaster.net', }, description: 'Dynamic QR code generator with analytics, branding, and bulk generation for modern marketing campaigns.', inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.com', + mainEntityOfPage: 'https://www.qrmaster.net', }; } @@ -71,19 +71,19 @@ export function websiteSchema() { return { '@context': 'https://schema.org', '@type': 'WebSite', - '@id': 'https://www.qrmaster.com/#website', + '@id': 'https://www.qrmaster.net/#website', name: 'QR Master', - url: 'https://www.qrmaster.com', + url: 'https://www.qrmaster.net', inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.com', + mainEntityOfPage: 'https://www.qrmaster.net', publisher: { - '@id': 'https://www.qrmaster.com/#organization', + '@id': 'https://www.qrmaster.net/#organization', }, potentialAction: { '@type': 'SearchAction', target: { '@type': 'EntryPoint', - urlTemplate: 'https://www.qrmaster.com/blog?q={search_term_string}', + urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}', }, 'query-input': 'required name=search_term_string', }, @@ -94,14 +94,14 @@ export function breadcrumbSchema(items: BreadcrumbItem[]) { return { '@context': 'https://schema.org', '@type': 'BreadcrumbList', - '@id': `https://www.qrmaster.com${items[items.length - 1]?.url}#breadcrumb`, + '@id': `https://www.qrmaster.net${items[items.length - 1]?.url}#breadcrumb`, inLanguage: 'en', - mainEntityOfPage: `https://www.qrmaster.com${items[items.length - 1]?.url}`, + mainEntityOfPage: `https://www.qrmaster.net${items[items.length - 1]?.url}`, itemListElement: items.map((item, index) => ({ '@type': 'ListItem', position: index + 1, name: item.name, - item: `https://www.qrmaster.com${item.url}`, + item: `https://www.qrmaster.net${item.url}`, })), }; } @@ -110,14 +110,14 @@ export function blogPostingSchema(post: BlogPost) { return { '@context': 'https://schema.org', '@type': 'BlogPosting', - '@id': `https://www.qrmaster.com/blog/${post.slug}#article`, + '@id': `https://www.qrmaster.net/blog/${post.slug}#article`, headline: post.title, description: post.description, image: post.image, datePublished: post.datePublished, dateModified: post.dateModified, inLanguage: 'en', - mainEntityOfPage: `https://www.qrmaster.com/blog/${post.slug}`, + mainEntityOfPage: `https://www.qrmaster.net/blog/${post.slug}`, author: { '@type': 'Person', name: post.author, @@ -126,19 +126,19 @@ export function blogPostingSchema(post: BlogPost) { publisher: { '@type': 'Organization', name: 'QR Master', - url: 'https://www.qrmaster.com', + url: 'https://www.qrmaster.net', logo: { '@type': 'ImageObject', - url: 'https://www.qrmaster.com/static/og-image.png', + url: 'https://www.qrmaster.net/static/og-image.png', width: 1200, height: 630, }, }, isPartOf: { '@type': 'Blog', - '@id': 'https://www.qrmaster.com/blog#blog', + '@id': 'https://www.qrmaster.net/blog#blog', name: 'QR Master Blog', - url: 'https://www.qrmaster.com/blog', + url: 'https://www.qrmaster.net/blog', }, }; } @@ -147,9 +147,9 @@ export function faqPageSchema(faqs: FAQItem[]) { return { '@context': 'https://schema.org', '@type': 'FAQPage', - '@id': 'https://www.qrmaster.com/faq#faqpage', + '@id': 'https://www.qrmaster.net/faq#faqpage', inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.com/faq', + mainEntityOfPage: 'https://www.qrmaster.net/faq', mainEntity: faqs.map((faq) => ({ '@type': 'Question', name: faq.question, @@ -165,11 +165,11 @@ export function productSchema(product: { name: string; description: string; offe return { '@context': 'https://schema.org', '@type': 'Product', - '@id': 'https://www.qrmaster.com/pricing#product', + '@id': 'https://www.qrmaster.net/pricing#product', name: product.name, description: product.description, inLanguage: 'en', - mainEntityOfPage: 'https://www.qrmaster.com/pricing', + mainEntityOfPage: 'https://www.qrmaster.net/pricing', brand: { '@type': 'Organization', name: 'QR Master', @@ -189,11 +189,11 @@ export function howToSchema(task: HowToTask) { return { '@context': 'https://schema.org', '@type': 'HowTo', - '@id': `https://www.qrmaster.com/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`, + '@id': `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`, name: task.name, description: task.description, inLanguage: 'en', - mainEntityOfPage: `https://www.qrmaster.com/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`, + mainEntityOfPage: `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`, totalTime: task.totalTime || 'PT5M', step: task.steps.map((step, index) => ({ '@type': 'HowToStep', diff --git a/src/styles/globals.css b/src/styles/globals.css index 7ad1b7d..28931ad 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,6 +2,40 @@ @tailwind components; @tailwind utilities; +@layer utilities { + /* Floating blob animation for hero background */ + @keyframes blob { + 0%, 100% { + transform: translate(0, 0) scale(1); + } + 25% { + transform: translate(20px, -30px) scale(1.1); + } + 50% { + transform: translate(-20px, 20px) scale(0.9); + } + 75% { + transform: translate(30px, 10px) scale(1.05); + } + } + + .animate-blob { + animation: blob 20s ease-in-out infinite; + } + + .animation-delay-2000 { + animation-delay: 2s; + } + + .animation-delay-4000 { + animation-delay: 4s; + } + + .animation-delay-6000 { + animation-delay: 6s; + } +} + :root { --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220;