Seobility und Ahrefs 100/100 score

This commit is contained in:
Timo Knuth
2026-01-13 08:49:35 +01:00
parent 5b74b7b405
commit ffe4cca5e5
56 changed files with 6204 additions and 1918 deletions

254
src/app/(app)/AppLayout.tsx Normal file
View File

@@ -0,0 +1,254 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { Footer } from '@/components/ui/Footer';
import { useTranslation } from '@/hooks/useTranslation';
interface User {
id: string;
name: string | null;
email: string;
plan: string | null;
}
export default function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const { t } = useTranslation();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
// Fetch user data on mount
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch('/api/user');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Error fetching user:', error);
}
};
fetchUser();
}, []);
const handleSignOut = async () => {
// Track logout event before clearing data
try {
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
trackEvent('user_logout');
resetUser(); // Reset PostHog user session
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Clear all cookies
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
// Clear localStorage
localStorage.clear();
// Redirect to home
router.push('/');
};
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
const getUserInitials = () => {
if (!user) return 'U';
if (user.name) {
const names = user.name.trim().split(' ');
if (names.length >= 2) {
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
}
return user.name.substring(0, 2).toUpperCase();
}
// Fallback to email
return user.email.substring(0, 1).toUpperCase();
};
// Get display name (first name or full name)
const getDisplayName = () => {
if (!user) return 'User';
if (user.name) {
return user.name;
}
// Fallback to email without domain
return user.email.split('@')[0];
};
const navigation = [
{
name: t('nav.dashboard'),
href: '/dashboard',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
name: t('nav.create_qr'),
href: '/create',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
),
},
{
name: t('nav.bulk_creation'),
href: '/bulk-creation',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
},
{
name: t('nav.analytics'),
href: '/analytics',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
name: t('nav.pricing'),
href: '/pricing',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
{
name: t('nav.settings'),
href: '/settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
];
return (
<div className="min-h-screen bg-gray-50">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<Link href="/" className="flex items-center space-x-2">
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900">QR Master</span>
</Link>
<button
className="lg:hidden"
onClick={() => setSidebarOpen(false)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="p-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{item.icon}
<span className="font-medium">{item.name}</span>
</Link>
);
})}
</nav>
</aside>
{/* Main content */}
<div className="lg:ml-64">
{/* Top bar */}
<header className="bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-4 py-3">
<button
className="lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="flex items-center space-x-4 ml-auto">
{/* User Menu */}
<Dropdown
align="right"
trigger={
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-primary-600">
{getUserInitials()}
</span>
</div>
<span className="hidden md:block font-medium">
{getDisplayName()}
</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
}
>
<DropdownItem onClick={handleSignOut}>
Sign Out
</DropdownItem>
</Dropdown>
</div>
</div>
</header>
{/* Page content */}
<main className="p-6">
{children}
</main>
{/* Footer */}
<Footer variant="dashboard" />
</div>
</div>
);
}

View File

@@ -1,254 +1,27 @@
'use client';
import type { Metadata } from 'next';
import '@/styles/globals.css';
import { Providers } from '@/components/Providers';
import AppLayout from './AppLayout';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { Footer } from '@/components/ui/Footer';
import { useTranslation } from '@/hooks/useTranslation';
export const metadata: Metadata = {
title: 'Dashboard | QR Master',
robots: { index: false, follow: false }, // Dashboard pages shouldn't be indexed generally
};
interface User {
id: string;
name: string | null;
email: string;
plan: string | null;
}
export default function AppLayout({
children,
export default function RootAppLayout({
children,
}: {
children: React.ReactNode;
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const { t } = useTranslation();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
// Fetch user data on mount
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch('/api/user');
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Error fetching user:', error);
}
};
fetchUser();
}, []);
const handleSignOut = async () => {
// Track logout event before clearing data
try {
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
trackEvent('user_logout');
resetUser(); // Reset PostHog user session
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Clear all cookies
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
// Clear localStorage
localStorage.clear();
// Redirect to home
router.push('/');
};
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
const getUserInitials = () => {
if (!user) return 'U';
if (user.name) {
const names = user.name.trim().split(' ');
if (names.length >= 2) {
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
}
return user.name.substring(0, 2).toUpperCase();
}
// Fallback to email
return user.email.substring(0, 1).toUpperCase();
};
// Get display name (first name or full name)
const getDisplayName = () => {
if (!user) return 'User';
if (user.name) {
return user.name;
}
// Fallback to email without domain
return user.email.split('@')[0];
};
const navigation = [
{
name: t('nav.dashboard'),
href: '/dashboard',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
name: t('nav.create_qr'),
href: '/create',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
),
},
{
name: t('nav.bulk_creation'),
href: '/bulk-creation',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
},
{
name: t('nav.analytics'),
href: '/analytics',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
name: t('nav.pricing'),
href: '/pricing',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
{
name: t('nav.settings'),
href: '/settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
];
return (
<div className="min-h-screen bg-gray-50">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<Link href="/" className="flex items-center space-x-2">
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900">QR Master</span>
</Link>
<button
className="lg:hidden"
onClick={() => setSidebarOpen(false)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="p-4 space-y-1">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
? 'bg-primary-50 text-primary-600'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{item.icon}
<span className="font-medium">{item.name}</span>
</Link>
);
})}
</nav>
</aside>
{/* Main content */}
<div className="lg:ml-64">
{/* Top bar */}
<header className="bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-4 py-3">
<button
className="lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="flex items-center space-x-4 ml-auto">
{/* User Menu */}
<Dropdown
align="right"
trigger={
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-primary-600">
{getUserInitials()}
</span>
</div>
<span className="hidden md:block font-medium">
{getDisplayName()}
</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
}
>
<DropdownItem onClick={handleSignOut}>
Sign Out
</DropdownItem>
</Dropdown>
</div>
</div>
</header>
{/* Page content */}
<main className="p-6">
{children}
</main>
{/* Footer */}
<Footer variant="dashboard" />
</div>
</div>
);
}
return (
<html lang="en">
<body className="font-sans">
<Providers>
<AppLayout>
{children}
</AppLayout>
</Providers>
</body>
</html>
);
}

View File

@@ -8,6 +8,9 @@ import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
// Note: Metadata is defined in a separate metadata.ts file for client components
// or the parent layout should be updated to allow indexing for this specific page.
export default function PricingPage() {
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null);

View File

@@ -1,11 +1,26 @@
export default function AuthLayout({
import '@/styles/globals.css';
import { Providers } from '@/components/Providers';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Authentication | QR Master',
description: 'Login or Sign Up to QR Master',
};
export default function AuthRootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
{children}
</div>
<html lang="en">
<body className="font-sans">
<Providers>
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white">
{children}
</div>
</Providers>
</body>
</html>
);
}

View File

@@ -0,0 +1,187 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
export default function LoginClientPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetchWithCsrf('/api/auth/simple-login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful login with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
});
trackEvent('user_login', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard';
router.push(redirectUrl);
router.refresh();
} else {
setError(data.error || 'Invalid email or password');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
<p className="text-gray-600 mt-2">Sign in to your account</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
Back to Home
</Link>
</div>
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
<span className="text-sm text-gray-600">Remember me</span>
</label>
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
Forgot password?
</Link>
</div>
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
{csrfLoading ? 'Loading...' : 'Sign In'}
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Don't have an account?{' '}
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
Sign up
</Link>
</p>
</div>
</CardContent>
</Card>
<p className="text-center text-sm text-gray-500 mt-6">
By signing in, you agree to our{' '}
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
Privacy Policy
</Link>
</p>
</div>
</div>
);
}

View File

@@ -1,187 +1,24 @@
'use client';
import React from 'react';
import type { Metadata } from 'next';
import LoginClientPage from './ClientPage';
import React, { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
export const metadata: Metadata = {
title: 'Login to QR Master | Access Your Dashboard',
description: 'Sign in to QR Master to create, manage, and track your QR codes. Access your dashboard and view analytics.',
alternates: {
canonical: 'https://www.qrmaster.net/login',
},
};
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { t } = useTranslation();
const { fetchWithCsrf, loading: csrfLoading } = useCsrf();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetchWithCsrf('/api/auth/simple-login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful login with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
});
trackEvent('user_login', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Check for redirect parameter
const redirectUrl = searchParams.get('redirect') || '/dashboard';
router.push(redirectUrl);
router.refresh();
} else {
setError(data.error || 'Invalid email or password');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
<p className="text-gray-600 mt-2">Sign in to your account</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
Back to Home
</Link>
</div>
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="mr-2" />
<span className="text-sm text-gray-600">Remember me</span>
</label>
<Link href="/forgot-password" className="text-sm text-primary-600 hover:text-primary-700">
Forgot password?
</Link>
</div>
<Button type="submit" className="w-full" loading={loading} disabled={csrfLoading || loading}>
{csrfLoading ? 'Loading...' : 'Sign In'}
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Don't have an account?{' '}
<Link href="/signup" className="text-primary-600 hover:text-primary-700 font-medium">
Sign up
</Link>
</p>
</div>
</CardContent>
</Card>
<p className="text-center text-sm text-gray-500 mt-6">
By signing in, you agree to our{' '}
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
Privacy Policy
</Link>
</p>
<>
<h1 className="sr-only">Login to QR Master Create & Track QR Codes</h1>
<div className="sr-only">
<h2>Secure Dashboard Access</h2>
<p>Log in to manage your dynamic QR codes, view scan analytics, and update destination URLs instantly.</p>
</div>
</div>
<LoginClientPage />
</>
);
}

View File

@@ -0,0 +1,208 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
export default function SignupClientPage() {
const router = useRouter();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
try {
const response = await fetchWithCsrf('/api/auth/signup', {
method: 'POST',
body: JSON.stringify({ name, email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful signup with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
signupMethod: 'email',
});
trackEvent('user_signup', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Redirect to dashboard
router.push('/dashboard');
router.refresh();
} else {
setError(data.error || 'Failed to create account');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
Back to Home
</Link>
</div>
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<Input
label="Full Name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
required
/>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Input
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Button type="submit" className="w-full" loading={loading}>
Create Account
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign up with Google
</Button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Already have an account?{' '}
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
Sign in
</Link>
</p>
</div>
</CardContent>
</Card>
<p className="text-center text-sm text-gray-500 mt-6">
By signing up, you agree to our{' '}
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
Privacy Policy
</Link>
</p>
</div>
</div>
);
}

View File

@@ -1,208 +1,24 @@
'use client';
import React from 'react';
import type { Metadata } from 'next';
import SignupClientPage from './ClientPage';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
export const metadata: Metadata = {
title: 'Create Free Account | QR Master',
description: 'Sign up for QR Master to create free QR codes. Start with tracking, customization, and bulk generation features.',
alternates: {
canonical: 'https://www.qrmaster.net/signup',
},
};
export default function SignupPage() {
const router = useRouter();
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
try {
const response = await fetchWithCsrf('/api/auth/signup', {
method: 'POST',
body: JSON.stringify({ name, email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store user in localStorage for client-side
localStorage.setItem('user', JSON.stringify(data.user));
// Track successful signup with PostHog
try {
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
identifyUser(data.user.id, {
email: data.user.email,
name: data.user.name,
plan: data.user.plan || 'FREE',
signupMethod: 'email',
});
trackEvent('user_signup', {
method: 'email',
email: data.user.email,
});
} catch (error) {
console.error('PostHog tracking error:', error);
}
// Redirect to dashboard
router.push('/dashboard');
router.refresh();
} else {
setError(data.error || 'Failed to create account');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = () => {
// Redirect to Google OAuth API route
window.location.href = '/api/auth/google';
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
<span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link>
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
Back to Home
</Link>
</div>
<Card>
<CardContent className="p-6">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<Input
label="Full Name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
required
/>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Input
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
/>
<Button type="submit" className="w-full" loading={loading}>
Create Account
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleGoogleSignIn}
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign up with Google
</Button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Already have an account?{' '}
<Link href="/login" className="text-primary-600 hover:text-primary-700 font-medium">
Sign in
</Link>
</p>
</div>
</CardContent>
</Card>
<p className="text-center text-sm text-gray-500 mt-6">
By signing up, you agree to our{' '}
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
Privacy Policy
</Link>
</p>
<>
<h1 className="sr-only">Sign Up for QR Master Free QR Code Generator</h1>
<div className="sr-only">
<h2>Get Started Free</h2>
<p>Create your account today. No credit card required for the free tier. Start generating trackable QR codes in minutes.</p>
</div>
</div>
<SignupClientPage />
</>
);
}

View File

@@ -0,0 +1,248 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import en from '@/i18n/en.json';
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close simple menus when path changes
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
}, [pathname]);
// Default to English for general marketing pages
const t = en;
const tools = [
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
];
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2.5 group">
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<QrCode className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{/* Tools Dropdown */}
<div
className="relative group px-3 py-2"
onMouseEnter={() => setToolsOpen(true)}
onMouseLeave={() => setToolsOpen(false)}
>
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{toolsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
>
<div className="grid grid-cols-3 gap-1">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
>
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
<tool.icon className="w-4 h-4" />
</div>
<div>
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
</div>
</Link>
))}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
<p className="text-xs text-slate-500 font-medium">{t.nav.all_free}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.features}
</Link>
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.pricing}
</Link>
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.blog}
</Link>
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.faq}
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.login}
</Link>
<Link href="/signup">
<Button className={cn(
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
)}>
{t.nav.cta || "Get Started Free"}
</Button>
</Link>
</div>
{/* Mobile Menu Button - Always dark */}
<button
className="md:hidden p-2 text-slate-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</nav>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
>
<div className="container mx-auto px-4 py-6 space-y-2">
{/* Free Tools Accordion */}
<button
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
>
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{mobileToolsOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
>
<tool.icon className={cn("w-4 h-4", tool.color)} />
{tool.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-px bg-slate-100 my-2"></div>
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.faq}</Link>
<div className="grid grid-cols-2 gap-4 pt-4">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
<Button variant="outline" className="w-full justify-center">{t.nav.login}</Button>
</Link>
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">{t.nav.cta}</Button>
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
{/* Main Content */}
<main className="pt-20">{children}</main>
{/* Footer */}
<Footer t={t} />
</div >
);
}

View File

@@ -50,7 +50,7 @@ const blogPosts: Record<string, BlogPostData> = {
{
name: 'Create a Dynamic QR Code',
text: 'Log into your QR Master dashboard and select "Create Dynamic QR Code". Enter your destination URL and customize design options.',
url: 'https://www.qrmaster.net/create',
url: 'https://www.qrmaster.net/signup',
},
{
name: 'Enable UTM Tracking',
@@ -278,7 +278,7 @@ const blogPosts: Record<string, BlogPostData> = {
<h4>Step-by-Step with QR Master:</h4>
<ol>
<li>Sign up for free at <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li>
<li>Sign up for free at <a href="/signup">qrmaster.net/signup</a></li>
<li>Create a dynamic QR code by clicking "Create QR Code" and selecting "Dynamic QR"</li>
<li>Enter the destination URL for your website, landing page, or content</li>
<li>Customize the design with your logo, brand colors, and custom frame</li>
@@ -354,7 +354,7 @@ app.get('/qr/:id', async (req, res) => {
<p>Privacy Note: Always hash IP addresses, respect Do Not Track headers, and comply with GDPR when collecting scan data.</p>
<div class="my-8">
<img src="/blog/1-boy.png" alt="Person scanning QR code with smartphone in office" class="rounded-lg shadow-lg w-full" />
<img src="/blog/1-boy.webp" alt="Person scanning QR code with smartphone in office" class="rounded-lg shadow-lg w-full" />
</div>
<h2>QR Code Tracking Tools Comparison</h2>
@@ -809,7 +809,7 @@ Tracking ✓ | Editable ✓ | Analytics ✓
</pre>
<div class="my-8">
<img src="/blog/2-body.png" alt="Business card with elegant QR code" class="rounded-lg shadow-lg w-full" />
<img src="/blog/2-body.webp" alt="Business card with elegant QR code" class="rounded-lg shadow-lg w-full" />
</div>
<h2>Static vs Dynamic QR Codes: Side-by-Side Comparison</h2>
@@ -1441,7 +1441,7 @@ Event Ticket 1 | https://event.com/ticket/1 | events, tickets
<h3>Step 2: Sign Up for QR Master</h3>
<ol>
<li>Go to <a href="https://www.qrmaster.net/signup">qrmaster.net/signup</a></li>
<li>Go to <a href="/signup">qrmaster.net/signup</a></li>
<li>Create free account (email + password)</li>
<li>Verify your email</li>
<li><strong>Free plan:</strong> Up to 3 dynamic QR codes (no bulk upload)</li>
@@ -1870,7 +1870,7 @@ const response = await fetch('https://api.qrmaster.net/v1/bulk', {
{
name: 'Generate a Dynamic QR Code',
text: 'Use QR Master to create a dynamic QR code. This allows you to update your menu URL anytime without reprinting codes.',
url: 'https://www.qrmaster.net/create',
url: 'https://www.qrmaster.net/signup',
},
{
name: 'Customize Your QR Code Design',
@@ -2098,7 +2098,7 @@ const response = await fetch('https://api.qrmaster.net/v1/bulk', {
<h2>How to Create a vCard QR Code</h2>
<h3>Step 1: Choose Your QR Code Type</h3>
<p>Go to the <a href="/create">QR Master generator</a> and select "Contact Card" or vCard type. Choose between static (data embedded) or dynamic (editable, trackable).</p>
<p>Go to the <a href="/signup">QR Master generator</a> and select "Contact Card" or vCard type. Choose between static (data embedded) or dynamic (editable, trackable).</p>
<h3>Step 2: Enter Your Information</h3>
<p>Fill in the contact form with your details. Required fields typically include:</p>
@@ -2156,7 +2156,7 @@ const response = await fetch('https://api.qrmaster.net/v1/bulk', {
<h2>Related Resources</h2>
<ul>
<li><a href="/create">QR Code Generator</a></li>
<li><a href="/signup">QR Code Generator</a></li>
<li><a href="/blog/dynamic-vs-static-qr-codes">Dynamic vs Static QR Codes</a></li>
<li><a href="/blog/qr-code-print-size-guide">QR Code Print Size Guide</a></li>
</ul>
@@ -2253,9 +2253,9 @@ const response = await fetch('https://api.qrmaster.net/v1/bulk', {
<ol>
<li><strong>Identify Your Goal:</strong> What do you want customers to do after scanning?</li>
<li><strong>Choose Code Type:</strong> Static for permanent content, dynamic for flexibility</li>
<li><strong>Create Your QR Code:</strong> Use <a href="/create">our generator</a> to design and customize</li>
<li><strong>Create Your QR Code:</strong> Use <a href="/signup">our generator</a> to design and customize</li>
<li><strong>Print at Right Size:</strong> Follow our <a href="/blog/qr-code-print-size-guide">print size guide</a></li>
<li><strong>Track Performance:</strong> Monitor scans in your <a href="/analytics">analytics dashboard</a></li>
<li><strong>Track Performance:</strong> Monitor scans in your <a href="/signup">analytics dashboard</a></li>
</ol>
<h2>Common Mistakes Small Businesses Make</h2>
@@ -2411,7 +2411,7 @@ const response = await fetch('https://api.qrmaster.net/v1/bulk', {
<div class="bg-gradient-to-br from-primary-50 to-primary-100 p-8 rounded-2xl my-12 border border-primary-200">
<h3 class="text-2xl font-bold text-gray-900 mb-4">Create Print-Ready QR Codes</h3>
<p class="text-lg text-gray-700 mb-6">Download high-resolution SVG and PNG files ready for any print application.</p>
<a href="/create" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create QR Code →</a>
<a href="/signup" class="inline-block bg-primary-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-primary-700 transition-colors">Create QR Code →</a>
</div>
<h2>Related Resources</h2>
@@ -2440,7 +2440,7 @@ export async function generateMetadata({ params }: { params: { slug: string } })
};
}
const title = truncateAtWord(`${post.title} - QR Analytics Tips`, 60);
const title = post.title;
const description = truncateAtWord(post.excerpt, 160);
return {

View File

@@ -18,7 +18,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Insights: Latest QR Strategies', 60);
const description = truncateAtWord(
'Expert guides on QR analytics, dynamic codes & smart marketing uses.',
'Expert guides on QR code analytics, dynamic vs static codes, bulk generation, and smart marketing use cases. Learn how to maximize your QR campaign ROI.',
160
);

View File

@@ -333,7 +333,7 @@ export default function BulkQRCodeGeneratorPage() {
Start Bulk Generation
</Button>
</Link>
<Link href="/create">
<Link href="/signup">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Try Single QR First
</Button>

View File

@@ -180,7 +180,7 @@ export default function DynamicQRCodeGeneratorPage() {
position: 2,
name: 'Generate QR Code',
text: 'Enter your destination URL and customize the design with your branding',
url: 'https://www.qrmaster.net/create',
url: 'https://www.qrmaster.net/signup',
},
{
'@type': 'HowToStep',
@@ -504,7 +504,7 @@ export default function DynamicQRCodeGeneratorPage() {
Get Started Free
</Button>
</Link>
<Link href="/create">
<Link href="/signup">
<Button size="lg" variant="outline" className="text-lg px-8 py-4 w-full sm:w-auto border-white text-white hover:bg-white/10">
Create QR Code Now
</Button>

View File

@@ -14,7 +14,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Master FAQ: Dynamic & Bulk QR', 60);
const description = truncateAtWord(
'All answers: dynamic QR, security, analytics, bulk, events & print.',
'Find answers about dynamic QR codes, scan tracking, security, bulk generation, and event QR codes. Everything you need to know about QR Master features.',
160
);

View File

@@ -1,248 +1,77 @@
'use client';
import type { Metadata } from 'next';
import '@/styles/globals.css';
import { Providers } from '@/components/Providers';
import MarketingLayout from './MarketingLayout';
// Import schema functions from library
import { organizationSchema, websiteSchema } from '@/lib/schema';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import en from '@/i18n/en.json';
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
export default function MarketingLayout({
children,
export const metadata: Metadata = {
metadataBase: new URL('https://www.qrmaster.net'),
title: {
default: 'QR Master Smart QR Generator & Analytics',
template: '%s | QR Master',
},
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics, branded QR, bulk QR generator',
robots: isIndexable
? { index: true, follow: true }
: { index: false, follow: false },
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/logo.svg', type: 'image/svg+xml' },
],
apple: '/logo.svg',
},
twitter: {
card: 'summary_large_image',
site: '@qrmaster',
images: ['https://www.qrmaster.net/static/og-image.png'],
},
openGraph: {
type: 'website',
siteName: 'QR Master',
title: 'QR Master Smart QR Generator & Analytics',
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
url: 'https://www.qrmaster.net',
images: [
{
url: 'https://www.qrmaster.net/static/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
},
],
locale: 'en_US',
},
};
export default function RootMarketingLayout({
children,
}: {
children: React.ReactNode;
children: React.ReactNode;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close simple menus when path changes
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
}, [pathname]);
// Default to English for general marketing pages
const t = en;
const tools = [
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
];
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2.5 group">
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<QrCode className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{/* Tools Dropdown */}
<div
className="relative group px-3 py-2"
onMouseEnter={() => setToolsOpen(true)}
onMouseLeave={() => setToolsOpen(false)}
>
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{toolsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
>
<div className="grid grid-cols-3 gap-1">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
>
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
<tool.icon className="w-4 h-4" />
</div>
<div>
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
</div>
</Link>
))}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
<p className="text-xs text-slate-500 font-medium">{t.nav.all_free}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.features}
</Link>
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.pricing}
</Link>
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.blog}
</Link>
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.faq}
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.login}
</Link>
<Link href="/signup">
<Button className={cn(
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
)}>
{t.nav.cta || "Get Started Free"}
</Button>
</Link>
</div>
{/* Mobile Menu Button - Always dark */}
<button
className="md:hidden p-2 text-slate-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</nav>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
>
<div className="container mx-auto px-4 py-6 space-y-2">
{/* Free Tools Accordion */}
<button
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
>
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{mobileToolsOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
>
<tool.icon className={cn("w-4 h-4", tool.color)} />
{tool.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-px bg-slate-100 my-2"></div>
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.faq}</Link>
<div className="grid grid-cols-2 gap-4 pt-4">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
<Button variant="outline" className="w-full justify-center">{t.nav.login}</Button>
</Link>
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">{t.nav.cta}</Button>
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
{/* Main Content */}
<main className="pt-20">{children}</main>
{/* Footer */}
<Footer t={t} />
</div >
);
return (
<html lang="en">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/>
</head>
<body className="font-sans">
<Providers>
<MarketingLayout>
{children}
</MarketingLayout>
</Providers>
</body>
</html>
);
}

View File

@@ -0,0 +1,643 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import {
Mail,
Users,
QrCode,
BarChart3,
TrendingUp,
Crown,
Activity,
Loader2,
Lock,
LogOut,
Zap,
Send,
CheckCircle2,
} from 'lucide-react';
interface AdminStats {
users: {
total: number;
premium: number;
newThisWeek: number;
newThisMonth: number;
recent: Array<{
email: string;
name: string | null;
plan: string;
createdAt: string;
}>;
};
qrCodes: {
total: number;
dynamic: number;
static: number;
active: number;
};
scans: {
total: number;
dynamicOnly: number;
avgPerDynamicQR: string;
};
newsletter: {
subscribers: number;
};
topQRCodes: Array<{
id: string;
title: string;
type: string;
scans: number;
owner: string;
createdAt: string;
}>;
}
export default function NewsletterClient() {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(true);
const [loginError, setLoginError] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true);
// Newsletter management state
const [newsletterData, setNewsletterData] = useState<{
total: number;
recent: Array<{ email: string; createdAt: string }>;
} | null>(null);
const [sendingBroadcast, setSendingBroadcast] = useState(false);
const [broadcastResult, setBroadcastResult] = useState<{
success: boolean;
message: string;
} | null>(null);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/admin/stats');
if (response.ok) {
setIsAuthenticated(true);
const data = await response.json();
setStats(data);
setLoading(false);
// Also fetch newsletter data
fetchNewsletterData();
} else {
setIsAuthenticated(false);
}
} catch (error) {
setIsAuthenticated(false);
} finally {
setIsAuthenticating(false);
}
};
const fetchNewsletterData = async () => {
try {
const response = await fetch('/api/newsletter/broadcast');
if (response.ok) {
const data = await response.json();
setNewsletterData(data);
}
} catch (error) {
console.error('Failed to fetch newsletter data:', error);
}
};
const handleSendBroadcast = async () => {
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
return;
}
setSendingBroadcast(true);
setBroadcastResult(null);
try {
const response = await fetch('/api/newsletter/broadcast', {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
setBroadcastResult({
success: true,
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
});
} else {
setBroadcastResult({
success: false,
message: data.error || 'Failed to send broadcast',
});
}
} catch (error) {
setBroadcastResult({
success: false,
message: 'Network error. Please try again.',
});
} finally {
setSendingBroadcast(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError('');
setIsAuthenticating(true);
try {
const response = await fetch('/api/newsletter/admin-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (response.ok) {
setIsAuthenticated(true);
await checkAuth();
} else {
const data = await response.json();
setLoginError(data.error || 'Invalid credentials');
}
} catch (error) {
setLoginError('Login failed. Please try again.');
} finally {
setIsAuthenticating(false);
}
};
const handleLogout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/');
};
// Login Screen
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
<Card className="w-full max-w-md p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground text-sm">
Sign in to access admin panel
</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@example.com"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
{loginError && (
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
)}
<Button
type="submit"
disabled={isAuthenticating}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{isAuthenticating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="mt-6 pt-6 border-t text-center">
<p className="text-xs text-muted-foreground">
Admin credentials required
</p>
</div>
</Card>
</div>
);
}
// Loading
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
// Admin Dashboard
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground">
Platform overview and statistics
</p>
</div>
<Button
onClick={handleLogout}
variant="outline"
className="flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Logout
</Button>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* All Time Users */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total Users</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Month</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisMonth || 0}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Week</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisWeek || 0}
</span>
</div>
</div>
</Card>
{/* Dynamic QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
Dynamic
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</Card>
{/* Total Scans */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">
{stats?.scans.dynamicOnly.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Avg per QR</span>
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Dynamic</span>
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</div>
</Card>
</div>
{/* Secondary Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{/* Total All Scans */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h3 className="text-2xl font-bold">
{stats?.scans.total.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Total All Scans</p>
</div>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
</div>
</div>
</Card>
{/* Premium Users */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
<p className="text-sm text-muted-foreground">Premium Users</p>
</div>
</div>
</Card>
</div>
{/* Bottom Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Top QR Codes</h3>
<p className="text-xs text-muted-foreground">Most scanned</p>
</div>
</div>
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
<div className="space-y-3">
{stats.topQRCodes.map((qr, index) => (
<div
key={qr.id}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white text-sm font-bold">
#{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{qr.title}</p>
<p className="text-xs text-muted-foreground truncate">
{qr.owner}
</p>
</div>
</div>
<div className="text-right flex-shrink-0 ml-4">
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">scans</p>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No QR codes yet</p>
)}
</Card>
{/* Recent Users */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Recent Users</h3>
<p className="text-xs text-muted-foreground">Latest signups</p>
</div>
</div>
{stats?.users.recent && stats.users.recent.length > 0 ? (
<div className="space-y-3">
{stats.users.recent.map((user, index) => (
<div
key={index}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-white text-xs font-bold">
{(user.name || user.email).charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
{user.name || user.email}
</p>
<p className="text-xs text-muted-foreground truncate">
{new Date(user.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<Badge
className={
user.plan === 'FREE'
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
}
>
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
{user.plan}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No users yet</p>
)}
</Card>
</div>
{/* Newsletter Management Section */}
<div className="mt-8">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Newsletter Management</h3>
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
<p className="text-xs text-muted-foreground">Total Subscribers</p>
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
Active
</Badge>
</div>
{/* Broadcast Section */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
<div className="flex items-start gap-3 mb-3">
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
<div>
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
<p className="text-sm text-muted-foreground">
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
This will inform them that the features are now available.
</p>
</div>
</div>
{/* Resend Free Tier Warning */}
{(newsletterData?.total || 0) > 100 && (
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<strong>Warning: Resend Free Limit</strong>
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
</div>
</div>
)}
{broadcastResult && (
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
}`}>
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
<span className="text-sm">{broadcastResult.message}</span>
</div>
)}
<Button
onClick={handleSendBroadcast}
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{sendingBroadcast ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="w-4 h-4 mr-2" />
Send Launch Notification to All
</>
)}
</Button>
</div>
{/* Recent Subscribers */}
<div>
<h4 className="font-medium mb-3">Recent Subscribers</h4>
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
<div className="space-y-2">
{newsletterData.recent.map((subscriber, index) => (
<div
key={index}
className="flex items-center justify-between py-2 border-b border-border last:border-0"
>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{subscriber.email}</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(subscriber.createdAt).toLocaleDateString()}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No subscribers yet</p>
)}
</div>
{/* Tip */}
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-muted-foreground">
💡 Tip: View all subscribers in{' '}
<a
href="http://localhost:5555"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 dark:text-purple-400 hover:underline"
>
Prisma Studio
</a>
{' '}(NewsletterSubscription table)
</p>
</div>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -1,643 +1,19 @@
'use client';
import React from 'react';
import type { Metadata } from 'next';
import NewsletterClient from './NewsletterClient';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import {
Mail,
Users,
QrCode,
BarChart3,
TrendingUp,
Crown,
Activity,
Loader2,
Lock,
LogOut,
Zap,
Send,
CheckCircle2,
} from 'lucide-react';
export const metadata: Metadata = {
title: 'Admin Dashboard | QR Master',
description: 'Admin restricted area.',
robots: {
index: false,
follow: false,
},
alternates: {
canonical: 'https://www.qrmaster.net/newsletter',
},
};
interface AdminStats {
users: {
total: number;
premium: number;
newThisWeek: number;
newThisMonth: number;
recent: Array<{
email: string;
name: string | null;
plan: string;
createdAt: string;
}>;
};
qrCodes: {
total: number;
dynamic: number;
static: number;
active: number;
};
scans: {
total: number;
dynamicOnly: number;
avgPerDynamicQR: string;
};
newsletter: {
subscribers: number;
};
topQRCodes: Array<{
id: string;
title: string;
type: string;
scans: number;
owner: string;
createdAt: string;
}>;
}
export default function AdminDashboard() {
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(true);
const [loginError, setLoginError] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true);
// Newsletter management state
const [newsletterData, setNewsletterData] = useState<{
total: number;
recent: Array<{ email: string; createdAt: string }>;
} | null>(null);
const [sendingBroadcast, setSendingBroadcast] = useState(false);
const [broadcastResult, setBroadcastResult] = useState<{
success: boolean;
message: string;
} | null>(null);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/admin/stats');
if (response.ok) {
setIsAuthenticated(true);
const data = await response.json();
setStats(data);
setLoading(false);
// Also fetch newsletter data
fetchNewsletterData();
} else {
setIsAuthenticated(false);
}
} catch (error) {
setIsAuthenticated(false);
} finally {
setIsAuthenticating(false);
}
};
const fetchNewsletterData = async () => {
try {
const response = await fetch('/api/newsletter/broadcast');
if (response.ok) {
const data = await response.json();
setNewsletterData(data);
}
} catch (error) {
console.error('Failed to fetch newsletter data:', error);
}
};
const handleSendBroadcast = async () => {
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
return;
}
setSendingBroadcast(true);
setBroadcastResult(null);
try {
const response = await fetch('/api/newsletter/broadcast', {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
setBroadcastResult({
success: true,
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
});
} else {
setBroadcastResult({
success: false,
message: data.error || 'Failed to send broadcast',
});
}
} catch (error) {
setBroadcastResult({
success: false,
message: 'Network error. Please try again.',
});
} finally {
setSendingBroadcast(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError('');
setIsAuthenticating(true);
try {
const response = await fetch('/api/newsletter/admin-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (response.ok) {
setIsAuthenticated(true);
await checkAuth();
} else {
const data = await response.json();
setLoginError(data.error || 'Invalid credentials');
}
} catch (error) {
setLoginError('Login failed. Please try again.');
} finally {
setIsAuthenticating(false);
}
};
const handleLogout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/');
};
// Login Screen
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
<Card className="w-full max-w-md p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground text-sm">
Sign in to access admin panel
</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@example.com"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
/>
</div>
{loginError && (
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
)}
<Button
type="submit"
disabled={isAuthenticating}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{isAuthenticating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="mt-6 pt-6 border-t text-center">
<p className="text-xs text-muted-foreground">
Admin credentials required
</p>
</div>
</Card>
</div>
);
}
// Loading
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
// Admin Dashboard
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
<div className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
<p className="text-muted-foreground">
Platform overview and statistics
</p>
</div>
<Button
onClick={handleLogout}
variant="outline"
className="flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Logout
</Button>
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* All Time Users */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total Users</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Month</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisMonth || 0}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">This Week</span>
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
+{stats?.users.newThisWeek || 0}
</span>
</div>
</div>
</Card>
{/* Dynamic QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
</div>
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
Dynamic
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</Card>
{/* Total Scans */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">
{stats?.scans.dynamicOnly.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<span className="text-xs text-muted-foreground">Avg per QR</span>
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
All Time
</Badge>
</div>
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
<div className="mt-3 pt-3 border-t space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Dynamic</span>
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Static</span>
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
</div>
</div>
</Card>
</div>
{/* Secondary Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{/* Total All Scans */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h3 className="text-2xl font-bold">
{stats?.scans.total.toLocaleString() || 0}
</h3>
<p className="text-sm text-muted-foreground">Total All Scans</p>
</div>
</div>
</Card>
{/* Total QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
<p className="text-sm text-muted-foreground">Total QR Codes</p>
</div>
</div>
</Card>
{/* Premium Users */}
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
<p className="text-sm text-muted-foreground">Premium Users</p>
</div>
</div>
</Card>
</div>
{/* Bottom Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top QR Codes */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Top QR Codes</h3>
<p className="text-xs text-muted-foreground">Most scanned</p>
</div>
</div>
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
<div className="space-y-3">
{stats.topQRCodes.map((qr, index) => (
<div
key={qr.id}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-white text-sm font-bold">
#{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{qr.title}</p>
<p className="text-xs text-muted-foreground truncate">
{qr.owner}
</p>
</div>
</div>
<div className="text-right flex-shrink-0 ml-4">
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">scans</p>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No QR codes yet</p>
)}
</Card>
{/* Recent Users */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-lg">Recent Users</h3>
<p className="text-xs text-muted-foreground">Latest signups</p>
</div>
</div>
{stats?.users.recent && stats.users.recent.length > 0 ? (
<div className="space-y-3">
{stats.users.recent.map((user, index) => (
<div
key={index}
className="flex items-center justify-between py-3 border-b border-border last:border-0"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-white text-xs font-bold">
{(user.name || user.email).charAt(0).toUpperCase()}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
{user.name || user.email}
</p>
<p className="text-xs text-muted-foreground truncate">
{new Date(user.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<Badge
className={
user.plan === 'FREE'
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
}
>
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
{user.plan}
</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No users yet</p>
)}
</Card>
</div>
{/* Newsletter Management Section */}
<div className="mt-8">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Newsletter Management</h3>
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
<p className="text-xs text-muted-foreground">Total Subscribers</p>
</div>
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
Active
</Badge>
</div>
{/* Broadcast Section */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
<div className="flex items-start gap-3 mb-3">
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
<div>
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
<p className="text-sm text-muted-foreground">
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
This will inform them that the features are now available.
</p>
</div>
</div>
{/* Resend Free Tier Warning */}
{(newsletterData?.total || 0) > 100 && (
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<strong>Warning: Resend Free Limit</strong>
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
</div>
</div>
)}
{broadcastResult && (
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
}`}>
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
<span className="text-sm">{broadcastResult.message}</span>
</div>
)}
<Button
onClick={handleSendBroadcast}
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
>
{sendingBroadcast ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="w-4 h-4 mr-2" />
Send Launch Notification to All
</>
)}
</Button>
</div>
{/* Recent Subscribers */}
<div>
<h4 className="font-medium mb-3">Recent Subscribers</h4>
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
<div className="space-y-2">
{newsletterData.recent.map((subscriber, index) => (
<div
key={index}
className="flex items-center justify-between py-2 border-b border-border last:border-0"
>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{subscriber.email}</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(subscriber.createdAt).toLocaleDateString()}
</span>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No subscribers yet</p>
)}
</div>
{/* Tip */}
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-muted-foreground">
💡 Tip: View all subscribers in{' '}
<a
href="http://localhost:5555"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 dark:text-purple-400 hover:underline"
>
Prisma Studio
</a>
{' '}(NewsletterSubscription table)
</p>
</div>
</Card>
</div>
</div>
</div>
);
export default function NewsletterPage() {
return <NewsletterClient />;
}

View File

@@ -16,7 +16,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Master: Dynamic QR Generator', 60);
const description = truncateAtWord(
'Dynamic QR, branding, bulk generation & analytics for all campaigns.',
'Create professional QR codes with QR Master. Dynamic QR with tracking, bulk generation, custom branding, and real-time analytics for all your campaigns.',
160
);
@@ -49,9 +49,11 @@ export default function HomePage() {
<>
<SeoJsonLd data={[organizationSchema(), websiteSchema(), generateFaqSchema(en.faq.questions)]} />
{/* Server-rendered H1 for SEO - visually hidden but crawlable */}
<h1 className="sr-only">QR Master: Dynamic QR Code Generator with Analytics</h1>
{/* Server-rendered SEO content for crawlers */}
<div className="sr-only" aria-hidden="false">
<h1>QR Master: Free Dynamic QR Code Generator with Tracking & Analytics</h1>
<p>
Create professional QR codes for your business with QR Master. Our dynamic QR code generator
lets you create trackable QR codes, edit destinations anytime, and view detailed analytics.

View File

@@ -0,0 +1,268 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
export default function PricingClient() {
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null);
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null);
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
useEffect(() => {
// Fetch current user plan
const fetchUserPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
const data = await response.json();
setCurrentPlan(data.plan || 'FREE');
setCurrentInterval(data.interval || null);
}
} catch (error) {
console.error('Error fetching user plan:', error);
}
};
fetchUserPlan();
}, []);
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
setLoading(plan);
try {
const response = await fetch('/api/stripe/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plan,
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
}),
});
if (!response.ok) {
throw new Error('Failed to create checkout session');
}
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Error creating checkout session:', error);
showToast('Failed to start checkout. Please try again.', 'error');
setLoading(null);
}
};
const handleDowngrade = async () => {
// Show confirmation dialog
const confirmed = window.confirm(
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
);
if (!confirmed) {
return;
}
setLoading('FREE');
try {
const response = await fetch('/api/stripe/cancel-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to cancel subscription');
}
showToast('Successfully downgraded to Free plan', 'success');
// Refresh to update the plan
setTimeout(() => {
window.location.reload();
}, 1500);
} catch (error: any) {
console.error('Error canceling subscription:', error);
showToast(error.message || 'Failed to downgrade. Please try again.', 'error');
setLoading(null);
}
};
// Helper function to check if this is the user's exact current plan (plan + interval)
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => {
return currentPlan === planType && currentInterval === interval;
};
// Helper function to check if user has this plan but different interval
const hasPlanDifferentInterval = (planType: string) => {
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod;
};
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
const plans = [
{
key: 'free',
name: 'Free',
price: '€0',
period: 'forever',
showDiscount: false,
features: [
'3 dynamic QR codes',
'Unlimited static QR codes',
'Basic scan tracking',
'Standard QR design templates',
'Download as SVG/PNG',
],
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
buttonVariant: 'outline' as const,
disabled: currentPlan === 'FREE',
popular: false,
onDowngrade: handleDowngrade,
},
{
key: 'pro',
name: 'Pro',
price: billingPeriod === 'month' ? '€9' : '€90',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'50 dynamic QR codes',
'Unlimited static QR codes',
'Advanced analytics (scans, devices, locations)',
'Custom branding (colors & logos)',
],
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('PRO')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Pro',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
popular: true,
onUpgrade: () => handleUpgrade('PRO'),
},
{
key: 'business',
name: 'Business',
price: billingPeriod === 'month' ? '€29' : '€290',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'500 dynamic QR codes',
'Unlimited static QR codes',
'Everything from Pro',
'Bulk QR Creation (up to 1,000)',
'Priority email support',
'Advanced tracking & insights',
],
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('BUSINESS')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Business',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
popular: false,
onUpgrade: () => handleUpgrade('BUSINESS'),
},
];
return (
<div className="container mx-auto px-4 py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Choose Your Plan
</h1>
<p className="text-xl text-gray-600">
Select the perfect plan for your QR code needs
</p>
</div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{plans.map((plan) => (
<Card
key={plan.key}
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<Badge variant="info" className="px-3 py-1">
Most Popular
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{plan.name}
</CardTitle>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.price}
</span>
<span className="text-gray-600 ml-2">
{plan.period}
</span>
</div>
{plan.showDiscount && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-6">
<ul className="space-y-3">
{plan.features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span className="text-gray-700">{feature}</span>
</li>
))}
</ul>
<Button
variant={plan.buttonVariant}
className="w-full"
size="lg"
disabled={plan.disabled || loading === plan.key.toUpperCase()}
onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade}
>
{loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText}
</Button>
</CardContent>
</Card>
))}
</div>
<div className="text-center mt-12">
<p className="text-gray-600">
All plans include unlimited static QR codes and basic customization.
</p>
<p className="text-gray-600 mt-2">
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import type { Metadata } from 'next';
import PricingClient from './PricingClient';
export const metadata: Metadata = {
title: 'Pricing Plans | QR Master',
description: 'Choose the perfect QR code plan for your needs. Free, Pro, and Business plans with dynamic QR codes, analytics, bulk generation, and custom branding.',
alternates: {
canonical: 'https://www.qrmaster.net/pricing',
},
robots: {
index: true,
follow: true,
},
};
export default function PricingPage() {
return (
<>
{/* Server-rendered H1 for SEO */}
<h1 className="sr-only">QR Master Pricing Choose Your QR Code Plan</h1>
<div className="sr-only">
<h2>Compare our plans</h2>
<p>Find the best QR code solution for your business. From free personal tiers to enterprise-grade dynamic code management.</p>
</div>
<PricingClient />
</>
);
}

View File

@@ -3,7 +3,7 @@ import Link from 'next/link';
export const metadata = {
title: 'Privacy Policy | QR Master',
description: 'Privacy Policy and data protection information for QR Master',
description: 'Read our Privacy Policy to understand how QR Master collects, uses, and protects your data. We are committed to GDPR compliance and data security.',
};
export default function PrivacyPage() {

View File

@@ -199,7 +199,7 @@ export default function QRCodeTrackingPage() {
Start Tracking Free
</Button>
</Link>
<Link href="/create">
<Link href="/signup">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
Create Trackable QR Code
</Button>

View File

@@ -4,6 +4,7 @@ import CryptoGenerator from './CryptoGenerator';
import { Bitcoin, Shield, Zap, Smartphone, Wallet, Coins, Sparkles, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -305,6 +306,9 @@ export default function CryptoQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import EmailGenerator from './EmailGenerator';
import { Mail, Zap, Smartphone, Lock, Download, Sparkles } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -233,6 +234,9 @@ export default function EmailPage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import EventGenerator from './EventGenerator';
import { Calendar, Shield, Zap, Smartphone, Clock, UserCheck, Download, Share2, Check } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -300,6 +301,9 @@ export default function EventQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import FacebookGenerator from './FacebookGenerator';
import { Facebook, Shield, Zap, Smartphone, ThumbsUp, Users, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -308,6 +309,9 @@ export default function FacebookQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import GeolocationGenerator from './GeolocationGenerator';
import { MapPin, Shield, Zap, Smartphone, Navigation, Map, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -303,6 +304,9 @@ export default function GeolocationQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import InstagramGenerator from './InstagramGenerator';
import { Instagram, Shield, Zap, Smartphone, Camera, Heart, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -297,6 +298,8 @@ export default function InstagramQRCodePage() {
</div>
</section>
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import PayPalGenerator from './PayPalGenerator';
import { CreditCard, Shield, Zap, Smartphone, DollarSign, Download, Share2, Banknote } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -293,6 +294,9 @@ export default function PayPalQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import PhoneGenerator from './PhoneGenerator';
import { Phone, Shield, Zap, Smartphone, PhoneCall, Download } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -305,6 +306,9 @@ export default function PhoneQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import SMSGenerator from './SMSGenerator';
import { MessageSquare, Shield, Zap, Smartphone, Send } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -262,6 +263,9 @@ export default function SMSQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import TeamsGenerator from './TeamsGenerator';
import { Users, Shield, Zap, Video, MessageCircle, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -278,6 +279,9 @@ export default function TeamsQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import TextGenerator from './TextGenerator';
import { Type, Shield, Zap, Smartphone, FileText, QrCode, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -295,6 +296,9 @@ export default function TextQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import TiktokGenerator from './TikTokGenerator';
import { Music, Shield, Zap, Smartphone, Video, Heart, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -305,6 +306,8 @@ export default function TiktokQRCodePage() {
</div>
</section>
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import TwitterGenerator from './TwitterGenerator';
import { Twitter, Shield, Zap, Smartphone, MessageCircle, UserPlus, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -306,6 +307,9 @@ export default function TwitterQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -1,9 +1,10 @@
import React from 'react';
import type { Metadata } from 'next';
import URLGenerator from './URLGenerator';
import { Link as LinkIcon, Shield, Zap, Smartphone, Globe, BarChart } from 'lucide-react';
import { Link as LinkIcon, Shield, Zap, Smartphone, Globe } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -254,6 +255,8 @@ export default function URLQRCodePage() {
</div>
</section>
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import VCardGenerator from './VCardGenerator';
import { User, Shield, Zap, Smartphone, Contact, Share2, Check, UserPlus } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -267,6 +268,9 @@ export default function VCardQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import WhatsappGenerator from './WhatsAppGenerator';
import { MessageCircle, Shield, Zap, Smartphone, Send, Phone, Download, Check } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -306,6 +307,9 @@ export default function WhatsappQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import WiFiGenerator from './WiFiGenerator';
import { Wifi, Shield, Zap, Smartphone, Lock, QrCode, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -311,6 +312,9 @@ export default function WiFiQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION - Featured Snippet Optimized */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import YoutubeGenerator from './YouTubeGenerator';
import { Youtube, Shield, Zap, Smartphone, Play, Radio, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -301,6 +302,9 @@ export default function YoutubeQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -4,6 +4,7 @@ import ZoomGenerator from './ZoomGenerator';
import { Video, Shield, Zap, Smartphone, Users, Download, Share2 } from 'lucide-react';
import { QRCodeSVG } from 'qrcode.react';
import { ToolBreadcrumb } from '@/components/seo/BreadcrumbSchema';
import { RelatedTools } from '@/components/marketing/RelatedTools';
// SEO Optimized Metadata
export const metadata: Metadata = {
@@ -278,6 +279,9 @@ export default function ZoomQRCodePage() {
</div>
</section>
{/* RELATED TOOLS */}
<RelatedTools />
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -0,0 +1,248 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import de from '@/i18n/de.json';
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close simple menus when path changes
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
}, [pathname]);
// Always German for this layout
const t = de;
const tools = [
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
];
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2.5 group">
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<QrCode className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{/* Tools Dropdown */}
<div
className="relative group px-3 py-2"
onMouseEnter={() => setToolsOpen(true)}
onMouseLeave={() => setToolsOpen(false)}
>
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{toolsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
>
<div className="grid grid-cols-3 gap-1">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
>
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
<tool.icon className="w-4 h-4" />
</div>
<div>
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
</div>
</Link>
))}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
<p className="text-xs text-slate-500 font-medium">{t.nav.all_free}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.features}
</Link>
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.pricing}
</Link>
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.blog}
</Link>
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.faq}
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.login}
</Link>
<Link href="/signup">
<Button className={cn(
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
)}>
{t.nav.cta || "Get Started Free"}
</Button>
</Link>
</div>
{/* Mobile Menu Button - Always dark */}
<button
className="md:hidden p-2 text-slate-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</nav>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
>
<div className="container mx-auto px-4 py-6 space-y-2">
{/* Free Tools Accordion */}
<button
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
>
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{mobileToolsOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
>
<tool.icon className={cn("w-4 h-4", tool.color)} />
{tool.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-px bg-slate-100 my-2"></div>
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.faq}</Link>
<div className="grid grid-cols-2 gap-4 pt-4">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
<Button variant="outline" className="w-full justify-center">{t.nav.login}</Button>
</Link>
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">{t.nav.cta}</Button>
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
{/* Main Content */}
<main className="pt-20">{children}</main>
{/* Footer */}
<Footer t={t} />
</div >
);
}

View File

@@ -1,248 +1,42 @@
'use client';
import type { Metadata } from 'next';
import '@/styles/globals.css';
import { Providers } from '@/components/Providers';
import MarketingDeLayout from './MarketingDeLayout';
import { organizationSchema, websiteSchema } from '@/lib/schema';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import de from '@/i18n/de.json';
import { ChevronDown, Wifi, Contact, MessageCircle, QrCode, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
export const metadata: Metadata = {
title: {
default: 'QR Master QR Code Generator & Analytics',
template: '%s | QR Master',
},
description: 'Erstellen Sie dynamische QR Codes, verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
robots: { index: true, follow: true },
};
export default function MarketingLayout({
export default function RootMarketingDeLayout({
children,
}: {
children: React.ReactNode;
}) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close simple menus when path changes
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
}, [pathname]);
// Always German for this layout
const t = de;
const tools = [
{ name: 'URL / Link', description: 'Link to any website', href: '/tools/url-qr-code', icon: Link2, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'Text', description: 'Plain text message', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'WiFi', description: 'Share WiFi credentials', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'VCard', description: 'Digital business card', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WhatsApp', description: 'Start a chat', href: '/tools/whatsapp-qr-code', icon: MessageCircle, color: 'text-green-500', bgColor: 'bg-green-50' },
{ name: 'Email', description: 'Compose an email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', description: 'Send a text message', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Phone', description: 'Start a call', href: '/tools/phone-qr-code', icon: Phone, color: 'text-violet-500', bgColor: 'bg-violet-50' },
{ name: 'Event', description: 'Add calendar event', href: '/tools/event-qr-code', icon: Calendar, color: 'text-red-500', bgColor: 'bg-red-50' },
{ name: 'Location', description: 'Share a place', href: '/tools/geolocation-qr-code', icon: MapPin, color: 'text-emerald-500', bgColor: 'bg-emerald-50' },
{ name: 'Facebook', description: 'Facebook profile/page', href: '/tools/facebook-qr-code', icon: Facebook, color: 'text-blue-600', bgColor: 'bg-blue-50' },
{ name: 'Instagram', description: 'Instagram profile', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'Twitter / X', description: 'Twitter profile', href: '/tools/twitter-qr-code', icon: Twitter, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'YouTube', description: 'YouTube video/channel', href: '/tools/youtube-qr-code', icon: Youtube, color: 'text-red-600', bgColor: 'bg-red-50' },
{ name: 'TikTok', description: 'TikTok profile', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
{ name: 'Crypto', description: 'Share wallet address', href: '/tools/crypto-qr-code', icon: Bitcoin, color: 'text-orange-500', bgColor: 'bg-orange-50' },
{ name: 'PayPal', description: 'Receive payments', href: '/tools/paypal-qr-code', icon: CreditCard, color: 'text-blue-700', bgColor: 'bg-blue-50' },
{ name: 'Zoom', description: 'Join Zoom meeting', href: '/tools/zoom-qr-code', icon: Video, color: 'text-sky-500', bgColor: 'bg-sky-50' },
{ name: 'Teams', description: 'Join Teams meeting', href: '/tools/teams-qr-code', icon: Users, color: 'text-violet-500', bgColor: 'bg-violet-50' },
];
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-2.5 group">
<div className="relative w-9 h-9 flex items-center justify-center bg-indigo-600 rounded-lg shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<QrCode className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{/* Tools Dropdown */}
<div
className="relative group px-3 py-2"
onMouseEnter={() => setToolsOpen(true)}
onMouseLeave={() => setToolsOpen(false)}
>
<button className="flex items-center space-x-1 text-sm font-medium text-slate-600 group-hover:text-slate-900 transition-colors">
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-4 h-4 transition-transform duration-200", toolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{toolsOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-[750px] bg-white rounded-2xl shadow-lg border border-slate-100 p-4 overflow-hidden"
>
<div className="grid grid-cols-3 gap-1">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-2.5 rounded-xl transition-colors hover:bg-slate-50"
>
<div className={cn("p-2 rounded-lg shrink-0", tool.bgColor, tool.color)}>
<tool.icon className="w-4 h-4" />
</div>
<div>
<div className="text-sm font-semibold text-slate-900">{tool.name}</div>
<p className="text-xs text-slate-500 leading-snug">{tool.description}</p>
</div>
</Link>
))}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 -mx-4 -mb-4 px-4 py-3 text-center bg-slate-50/50">
<p className="text-xs text-slate-500 font-medium">{t.nav.all_free}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link href="/#features" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.features}
</Link>
<Link href="/#pricing" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.pricing}
</Link>
<Link href="/blog" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.blog}
</Link>
<Link href="/#faq" className="px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.faq}
</Link>
</div>
<div className="hidden md:flex items-center space-x-4">
<Link href="/login" className="text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors">
{t.nav.login}
</Link>
<Link href="/signup">
<Button className={cn(
"font-semibold shadow-lg shadow-indigo-500/20 transition-all hover:scale-105",
scrolled ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-blue-600 text-white hover:bg-blue-700"
)}>
{t.nav.cta || "Get Started Free"}
</Button>
</Link>
</div>
{/* Mobile Menu Button - Always dark */}
<button
className="md:hidden p-2 text-slate-900"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</nav>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-white border-b border-slate-100 overflow-hidden"
>
<div className="container mx-auto px-4 py-6 space-y-2">
{/* Free Tools Accordion */}
<button
onClick={() => setMobileToolsOpen(!mobileToolsOpen)}
className="flex items-center justify-between w-full px-4 py-3 rounded-xl hover:bg-slate-50 text-slate-700 font-semibold"
>
<span>{t.nav.tools}</span>
<ChevronDown className={cn("w-5 h-5 transition-transform", mobileToolsOpen && "rotate-180")} />
</button>
<AnimatePresence>
{mobileToolsOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="max-h-[50vh] overflow-y-auto pl-4 space-y-1 border-l-2 border-slate-100 ml-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-slate-50 text-slate-600 text-sm"
onClick={() => { setMobileMenuOpen(false); setMobileToolsOpen(false); }}
>
<tool.icon className={cn("w-4 h-4", tool.color)} />
{tool.name}
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="h-px bg-slate-100 my-2"></div>
<Link href="/#features" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.features}</Link>
<Link href="/#pricing" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.pricing}</Link>
<Link href="/blog" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.blog}</Link>
<Link href="/#faq" className="block px-4 py-3 text-slate-700 font-medium rounded-xl hover:bg-slate-50" onClick={() => setMobileMenuOpen(false)}>{t.nav.faq}</Link>
<div className="grid grid-cols-2 gap-4 pt-4">
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
<Button variant="outline" className="w-full justify-center">{t.nav.login}</Button>
</Link>
<Link href="/signup" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full justify-center bg-indigo-600 hover:bg-indigo-700">{t.nav.cta}</Button>
</Link>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
{/* Main Content */}
<main className="pt-20">{children}</main>
{/* Footer */}
<Footer t={t} />
</div >
<html lang="de">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/>
</head>
<body className="font-sans">
<Providers>
<MarketingDeLayout>
{children}
</MarketingDeLayout>
</Providers>
</body>
</html>
);
}

View File

@@ -20,11 +20,8 @@ function truncateAtWord(text: string, maxLength: number): string {
}
export async function generateMetadata(): Promise<Metadata> {
const title = truncateAtWord('QR Code Erstellen Kostenlos & Sofort | QR Master', 60);
const description = truncateAtWord(
'Erstellen Sie QR Codes kostenlos in Sekunden. Statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Für immer kostenlos.',
160
);
const title = 'QR Code Erstellen Kostenlos | QR Master';
const description = 'Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.';
return {
title,
@@ -69,9 +66,11 @@ export default function QRCodeErstellenPage() {
<>
<SeoJsonLd data={[organizationSchema(), websiteSchema(), generateFaqSchema(t.faq.questions)]} />
{/* Server-rendered H1 for SEO - visually hidden but crawlable */}
<h1 className="sr-only">QR Code Erstellen Kostenloser QR Code Generator mit Tracking</h1>
{/* Server-rendered SEO content for crawlers - GERMAN */}
<div className="sr-only" aria-hidden="false">
<h1>QR Code Erstellen Kostenloser QR Code Generator mit Tracking & Statistiken</h1>
<p>
Erstellen Sie professionelle QR Codes für Ihr Unternehmen mit QR Master. Unser dynamischer QR Code Generator
ermöglicht es Ihnen, trackbare QR Codes zu erstellen, Ziel-URLs jederzeit zu ändern und detaillierte Statistiken einzusehen.

View File

@@ -1,133 +0,0 @@
import type { Metadata } from 'next';
import { Suspense } from 'react';
import '@/styles/globals.css';
import { ToastContainer } from '@/components/ui/Toast';
import AuthProvider from '@/components/SessionProvider';
import { PostHogProvider } from '@/components/PostHogProvider';
import CookieBanner from '@/components/CookieBanner';
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
// Organization Schema for all pages
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': 'https://www.qrmaster.net/#organization',
name: 'QR Master',
alternateName: 'QRMaster',
url: 'https://www.qrmaster.net',
logo: {
'@type': 'ImageObject',
url: 'https://www.qrmaster.net/static/og-image.png',
width: 1200,
height: 630,
},
image: 'https://www.qrmaster.net/static/og-image.png',
sameAs: ['https://twitter.com/qrmaster'],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'Customer Support',
email: 'support@qrmaster.net',
availableLanguage: ['English', 'German'],
},
description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.',
slogan: 'Dynamic QR codes that work smarter',
foundingDate: '2025',
areaServed: 'Worldwide',
serviceType: 'Software as a Service',
priceRange: '$0 - $29',
};
// Website Schema for all pages
const websiteSchema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': 'https://www.qrmaster.net/#website',
name: 'QR Master',
url: 'https://www.qrmaster.net',
inLanguage: 'en',
publisher: { '@id': 'https://www.qrmaster.net/#organization' },
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}',
},
'query-input': 'required name=search_term_string',
},
};
export const metadata: Metadata = {
metadataBase: new URL('https://www.qrmaster.net'),
title: {
default: 'QR Master Smart QR Generator & Analytics',
template: '%s | QR Master',
},
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
keywords: 'QR code, QR generator, dynamic QR, QR tracking, QR analytics, branded QR, bulk QR generator',
robots: isIndexable
? { index: true, follow: true }
: { index: false, follow: false },
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/logo.svg', type: 'image/svg+xml' },
],
apple: '/logo.svg',
},
twitter: {
card: 'summary_large_image',
site: '@qrmaster',
images: ['https://www.qrmaster.net/static/og-image.png'],
},
openGraph: {
type: 'website',
siteName: 'QR Master',
title: 'QR Master Smart QR Generator & Analytics',
description: 'Create dynamic QR codes, track scans, and scale campaigns with secure analytics.',
url: 'https://www.qrmaster.net',
images: [
{
url: 'https://www.qrmaster.net/static/og-image.png',
width: 1200,
height: 630,
alt: 'QR Master - Dynamic QR Code Generator and Analytics Platform',
},
],
locale: 'en_US',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
{/* AdSense script removed from global layout to prevent auto-ads on landing pages.
Ads are loaded locally via the AdBanner component only on tool pages. */}
</head>
<body className="font-sans">
<Suspense fallback={null}>
<PostHogProvider>
<AuthProvider>
{children}
</AuthProvider>
<CookieBanner />
<ToastContainer />
</PostHogProvider>
</Suspense>
</body>
</html>
);
}

View File

@@ -40,6 +40,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/qr-code-erstellen`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/qr-code-tracking`,
lastModified: new Date(),

View File

@@ -0,0 +1,21 @@
'use client';
import { Suspense } from 'react';
import { ToastContainer } from '@/components/ui/Toast';
import AuthProvider from '@/components/SessionProvider';
import { PostHogProvider } from '@/components/PostHogProvider';
import CookieBanner from '@/components/CookieBanner';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={null}>
<PostHogProvider>
<AuthProvider>
{children}
</AuthProvider>
<CookieBanner />
<ToastContainer />
</PostHogProvider>
</Suspense>
);
}

View File

@@ -0,0 +1,59 @@
import Link from 'next/link';
import {
Link as LinkIcon,
Type,
Wifi,
Contact,
MessageCircle,
Mail,
MessageSquare,
Phone,
Calendar,
MapPin,
Facebook,
Instagram,
Twitter,
Youtube,
Music,
Bitcoin,
CreditCard,
Video,
Users
} from 'lucide-react';
const tools = [
{ name: 'URL / Link', href: '/tools/url-qr-code', icon: LinkIcon, color: 'text-blue-500', bgColor: 'bg-blue-50' },
{ name: 'vCard', href: '/tools/vcard-qr-code', icon: Contact, color: 'text-pink-500', bgColor: 'bg-pink-50' },
{ name: 'WiFi', href: '/tools/wifi-qr-code', icon: Wifi, color: 'text-indigo-500', bgColor: 'bg-indigo-50' },
{ name: 'Text', href: '/tools/text-qr-code', icon: Type, color: 'text-slate-500', bgColor: 'bg-slate-50' },
{ name: 'Email', href: '/tools/email-qr-code', icon: Mail, color: 'text-amber-500', bgColor: 'bg-amber-50' },
{ name: 'SMS', href: '/tools/sms-qr-code', icon: MessageSquare, color: 'text-cyan-500', bgColor: 'bg-cyan-50' },
{ name: 'Instagram', href: '/tools/instagram-qr-code', icon: Instagram, color: 'text-pink-600', bgColor: 'bg-pink-50' },
{ name: 'TikTok', href: '/tools/tiktok-qr-code', icon: Music, color: 'text-slate-800', bgColor: 'bg-slate-100' },
];
export function RelatedTools() {
return (
<section className="py-12 bg-slate-50 border-t border-slate-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-slate-900 mb-8 text-center">
More Free QR Code Generators
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{tools.map((tool) => (
<Link
key={tool.name}
href={tool.href}
className="flex items-center space-x-3 p-4 bg-white rounded-xl shadow-sm border border-slate-100 transition-all hover:shadow-md hover:border-slate-200 hover:-translate-y-1"
>
<div className={`p-2 rounded-lg shrink-0 ${tool.bgColor} ${tool.color}`}>
<tool.icon className="w-5 h-5" />
</div>
<span className="font-semibold text-slate-700 text-sm">{tool.name}</span>
</Link>
))}
</div>
</div>
</section>
);
}

View File

@@ -16,6 +16,9 @@ export function middleware(req: NextRequest) {
'/newsletter',
'/tools',
'/qr-code-erstellen',
'/dynamic-qr-code-generator',
'/bulk-qr-code-generator',
'/qr-code-tracking',
];
// Check if path is public