feat: implement pricing strategy, subscription tiers, and core infrastructure for QR code management

This commit is contained in:
Timo Knuth
2026-04-14 19:34:47 +02:00
parent 82101ca08f
commit 6b73ac5c50
16 changed files with 1718 additions and 1344 deletions

View File

@@ -121,6 +121,7 @@ enum ContentType {
APP APP
COUPON COUPON
FEEDBACK FEEDBACK
BARCODE
} }
enum QRStatus { enum QRStatus {

View File

@@ -22,8 +22,10 @@ interface BulkQRData {
interface GeneratedQR { interface GeneratedQR {
title: string; title: string;
content: string; // Original URL content: string;
svg: string; // SVG markup svg: string;
slug?: string;
redirectUrl?: string;
} }
export default function BulkCreationPage() { export default function BulkCreationPage() {
@@ -35,16 +37,25 @@ export default function BulkCreationPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]); const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
const [userPlan, setUserPlan] = useState<string>('FREE'); const [userPlan, setUserPlan] = useState<string>('FREE');
const [isDynamic, setIsDynamic] = useState(false);
const [remainingDynamic, setRemainingDynamic] = useState(0);
// Check user plan on mount // Check user plan and dynamic quota on mount
React.useEffect(() => { React.useEffect(() => {
const checkPlan = async () => { const checkPlan = async () => {
try { try {
const response = await fetch('/api/user/plan'); const [planRes, statsRes] = await Promise.all([
if (response.ok) { fetch('/api/user/plan'),
const data = await response.json(); fetch('/api/user/stats'),
]);
if (planRes.ok) {
const data = await planRes.json();
setUserPlan(data.plan || 'FREE'); setUserPlan(data.plan || 'FREE');
} }
if (statsRes.ok) {
const stats = await statsRes.json();
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
}
} catch (error) { } catch (error) {
console.error('Error checking plan:', error); console.error('Error checking plan:', error);
} }
@@ -196,6 +207,58 @@ export default function BulkCreationPage() {
} }
}; };
const generateDynamicQRCodes = async () => {
setLoading(true);
const toProcess = remainingDynamic > 0 ? data.slice(0, remainingDynamic) : [];
if (toProcess.length === 0) {
showToast('Du hast keine dynamischen QR-Codes mehr übrig. Bitte upgrade deinen Plan.', 'error');
setLoading(false);
return;
}
if (data.length > remainingDynamic) {
showToast(`Nur ${remainingDynamic} dynamische Codes verfügbar. Es werden nur die ersten ${remainingDynamic} Zeilen verarbeitet.`, 'warning');
}
try {
const QRCode = require('qrcode');
const results: GeneratedQR[] = [];
for (const row of toProcess) {
const title = String(row[mapping.title as keyof typeof row] || 'Untitled');
const url = String(row[mapping.content as keyof typeof row] || 'https://example.com');
const res = await fetchWithCsrf('/api/qrs', {
method: 'POST',
body: JSON.stringify({
title,
contentType: 'URL',
content: { url },
isStatic: false,
}),
});
if (res.ok) {
const qr = await res.json();
const redirectUrl = `${window.location.origin}/r/${qr.slug}`;
const svg = await QRCode.toString(redirectUrl, { type: 'svg', width: 300, margin: 2 });
results.push({ title, content: url, svg, slug: qr.slug, redirectUrl });
}
}
setGeneratedQRs(results);
setRemainingDynamic(prev => Math.max(0, prev - results.length));
setStep('complete');
showToast(`${results.length} dynamische QR-Codes erstellt!`, 'success');
} catch (error) {
console.error('Dynamic QR generation error:', error);
showToast('Fehler beim Erstellen der dynamischen QR-Codes', 'error');
} finally {
setLoading(false);
}
};
const downloadAllQRCodes = async () => { const downloadAllQRCodes = async () => {
const zip = new JSZip(); const zip = new JSZip();
@@ -204,6 +267,18 @@ export default function BulkCreationPage() {
zip.file(fileName, qr.svg); zip.file(fileName, qr.svg);
}); });
// Add metadata CSV for dynamic QR codes
const hasDynamic = generatedQRs.some(qr => qr.slug);
if (hasDynamic) {
const csvRows = ['title,original_url,redirect_url,slug'];
generatedQRs.forEach(qr => {
if (qr.slug) {
csvRows.push(`"${qr.title}","${qr.content}","${qr.redirectUrl}","${qr.slug}"`);
}
});
zip.file('metadata.csv', csvRows.join('\n'));
}
const blob = await zip.generateAsync({ type: 'blob' }); const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, 'qr-codes-bulk.zip'); saveAs(blob, 'qr-codes-bulk.zip');
showToast('Download started!', 'success'); showToast('Download started!', 'success');
@@ -274,8 +349,8 @@ export default function BulkCreationPage() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
// Show upgrade prompt if not Business plan // Show upgrade prompt if not Business or Enterprise plan
if (userPlan !== 'BUSINESS') { if (userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') {
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<Card className="mt-12"> <Card className="mt-12">
@@ -309,6 +384,39 @@ export default function BulkCreationPage() {
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1> <h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p> <p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
{/* Static / Dynamic Toggle */}
<div className="mt-4 flex items-center gap-4 p-4 bg-gray-50 rounded-xl border border-gray-200">
<span className="text-sm font-medium text-gray-700">QR Code Type:</span>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={!isDynamic}
onChange={() => setIsDynamic(false)}
className="accent-primary-600"
/>
<span className="text-sm font-medium">Static</span>
<span className="text-xs text-gray-500">(download only, no tracking)</span>
</label>
<label className={`flex items-center gap-2 ${userPlan === 'BUSINESS' || userPlan === 'ENTERPRISE' ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'}`}>
<input
type="radio"
checked={isDynamic}
onChange={() => setIsDynamic(true)}
disabled={userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE'}
className="accent-primary-600"
/>
<span className="text-sm font-medium">Dynamic</span>
{isDynamic && remainingDynamic > 0 && (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
{remainingDynamic} verbleibend
</span>
)}
{(userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') && (
<span className="text-xs text-amber-600">(Business Plan erforderlich)</span>
)}
</label>
</div>
</div> </div>
{/* Template Warning Banner */} {/* Template Warning Banner */}
@@ -641,8 +749,13 @@ export default function BulkCreationPage() {
<Button variant="outline" onClick={() => setStep('upload')}> <Button variant="outline" onClick={() => setStep('upload')}>
Back Back
</Button> </Button>
<Button onClick={generateStaticQRCodes} loading={loading}> <Button
Generate {data.length} Static QR Codes onClick={isDynamic ? generateDynamicQRCodes : generateStaticQRCodes}
loading={loading}
>
{isDynamic
? `Generate ${Math.min(data.length, remainingDynamic)} Dynamic QR Codes`
: `Generate ${data.length} Static QR Codes`}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>

View File

@@ -15,8 +15,9 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast'; import { showToast } from '@/components/ui/Toast';
import { import {
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload, Barcode as BarcodeIcon
} from 'lucide-react'; } from 'lucide-react';
import Barcode from 'react-barcode';
// Tooltip component for form field help // Tooltip component for form field help
const Tooltip = ({ text }: { text: string }) => ( const Tooltip = ({ text }: { text: string }) => (
@@ -140,6 +141,7 @@ export default function CreatePage() {
{ value: 'APP', label: 'App Download', icon: Smartphone }, { value: 'APP', label: 'App Download', icon: Smartphone },
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket }, { value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star }, { value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
{ value: 'BARCODE', label: 'Barcode', icon: BarcodeIcon },
]; ];
// Get QR content based on content type // Get QR content based on content type
@@ -170,6 +172,8 @@ export default function CreatePage() {
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`; return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
case 'FEEDBACK': case 'FEEDBACK':
return content.feedbackUrl || 'https://example.com/feedback'; return content.feedbackUrl || 'https://example.com/feedback';
case 'BARCODE':
return content.value || '123456789';
default: default:
return 'https://example.com'; return 'https://example.com';
} }
@@ -642,6 +646,68 @@ export default function CreatePage() {
/> />
</> </>
); );
case 'BARCODE':
return (
<>
{isDynamic ? (
<>
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800">
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
(e.g. <span className="font-mono text-xs">qrmaster.net/r/</span>). When scanned with a
smartphone camera, it opens the browser and redirects to your destination which you
can update anytime. Works with smartphone cameras, not POS laser scanners.
</div>
<Input
label="Destination URL"
value={content.url || ''}
onChange={(e) => setContent({ ...content, url: e.target.value })}
placeholder="https://example.com"
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
<select
value={['CODE128', 'CODE39'].includes(content.format) ? content.format : 'CODE128'}
onChange={(e) => setContent({ ...content, format: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="CODE128">CODE128 General purpose (recommended)</option>
<option value="CODE39">CODE39 Industrial / logistics</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Only URL-capable formats available. EAN-13, UPC, and ITF-14 encode numbers only and cannot embed a redirect URL.
</p>
</div>
</>
) : (
<>
<Input
label="Barcode Value"
value={content.value || ''}
onChange={(e) => setContent({ ...content, value: e.target.value })}
placeholder="123456789012"
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Barcode Format</label>
<select
value={content.format || 'CODE128'}
onChange={(e) => setContent({ ...content, format: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="CODE128">CODE128 General purpose (recommended)</option>
<option value="EAN13">EAN-13 Retail products (international)</option>
<option value="UPC">UPC Retail products (USA/Canada)</option>
<option value="CODE39">CODE39 Industrial / logistics</option>
<option value="ITF14">ITF-14 Shipping containers</option>
<option value="MSI">MSI Shelf labeling / inventory</option>
<option value="pharmacode">Pharmacode Pharmaceutical packaging</option>
</select>
</div>
</>
)}
</>
);
default: default:
return null; return null;
} }
@@ -992,7 +1058,25 @@ export default function CreatePage() {
</div> </div>
)} )}
{qrContent ? ( {contentType === 'BARCODE' ? (
qrContent ? (
<div className="p-2 bg-white">
<Barcode
value={qrContent}
format={content.format || 'CODE128'}
lineColor={foregroundColor}
background={backgroundColor}
width={2}
height={100}
displayValue={true}
/>
</div>
) : (
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">
Enter barcode value
</div>
)
) : qrContent ? (
<div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}> <div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
<QRCodeSVG <QRCodeSVG
value={qrContent} value={qrContent}

View File

@@ -165,6 +165,8 @@ export default function SettingsPage() {
return { dynamic: 50, price: '€9', period: 'per month' }; return { dynamic: 50, price: '€9', period: 'per month' };
case 'BUSINESS': case 'BUSINESS':
return { dynamic: 500, price: '€29', period: 'per month' }; return { dynamic: 500, price: '€29', period: 'per month' };
case 'ENTERPRISE':
return { dynamic: 99999, price: 'Custom', period: 'per month' };
default: default:
return { dynamic: 3, price: '€0', period: 'forever' }; return { dynamic: 3, price: '€0', period: 'forever' };
} }

View File

@@ -9,11 +9,11 @@ import { Button } from '@/components/ui/Button';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf'; import { useCsrf } from '@/hooks/useCsrf';
type LoginClientProps = { type LoginClientProps = {
showPageHeading?: boolean; showPageHeading?: boolean;
}; };
export default function LoginClient({ showPageHeading = true }: LoginClientProps) { export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -22,6 +22,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -79,15 +80,15 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> <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="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6"> <Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" /> <img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span> <span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link> </Link>
{showPageHeading ? ( {showPageHeading ? (
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1> <h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
) : ( ) : (
<h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2> <h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2>
)} )}
<p className="text-gray-600 mt-2">Sign in to your account</p> <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"> <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 Back to Home
@@ -112,14 +113,37 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
required required
/> />
<Input <div className="space-y-1">
label="Password" <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
type="password" <div className="relative">
value={password} <input
onChange={(e) => setPassword(e.target.value)} id="password"
placeholder="••••••••" type={showPassword ? 'text' : 'password'}
required value={password}
/> onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="flex items-center"> <label className="flex items-center">

View File

@@ -19,6 +19,8 @@ export default function SignupClient() {
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -88,10 +90,10 @@ export default function SignupClient() {
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-white flex items-center justify-center p-4"> <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="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<Link href="/" className="inline-flex items-center space-x-2 mb-6"> <Link href="/" className="inline-flex items-center space-x-2 mb-6">
<img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" /> <img src="/favicon1.png" alt="QR Master" className="w-10 h-10 rounded-full object-cover" />
<span className="text-2xl font-bold text-gray-900">QR Master</span> <span className="text-2xl font-bold text-gray-900">QR Master</span>
</Link> </Link>
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1> <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> <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"> <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">
@@ -126,23 +128,69 @@ export default function SignupClient() {
required required
/> />
<Input <div className="space-y-1">
label="Password" <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
type="password" <div className="relative">
value={password} <input
onChange={(e) => setPassword(e.target.value)} id="password"
placeholder="••••••••" type={showPassword ? 'text' : 'password'}
required value={password}
/> onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<Input <div className="space-y-1">
label="Confirm Password" <label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">Confirm Password</label>
type="password" <div className="relative">
value={confirmPassword} <input
onChange={(e) => setConfirmPassword(e.target.value)} id="confirm-password"
placeholder="••••••••" type={showConfirmPassword ? 'text' : 'password'}
required value={confirmPassword}
/> onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
className="flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
tabIndex={-1}
>
{showConfirmPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
<Button type="submit" className="w-full" loading={loading}> <Button type="submit" className="w-full" loading={loading}>
Create Account Create Account

View File

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

View File

@@ -51,6 +51,7 @@ const PLAN_LIMITS = {
FREE: 3, FREE: 3,
PRO: 50, PRO: 50,
BUSINESS: 500, BUSINESS: 500,
ENTERPRISE: 99999,
}; };
// POST /api/qrs - Create a new QR code // POST /api/qrs - Create a new QR code

View File

@@ -1,62 +1,64 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const userId = cookies().get('userId')?.value; const userId = cookies().get('userId')?.value;
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
// Get user with plan info // Get user with plan info
const user = await db.user.findUnique({ const user = await db.user.findUnique({
where: { id: userId }, where: { id: userId },
select: { select: {
plan: true, plan: true,
}, },
}); });
if (!user) { if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 }); return NextResponse.json({ error: 'User not found' }, { status: 404 });
} }
// Count dynamic QR codes // Count dynamic QR codes
const dynamicQRCount = await db.qRCode.count({ const dynamicQRCount = await db.qRCode.count({
where: { where: {
userId, userId,
type: 'DYNAMIC', type: 'DYNAMIC',
}, },
}); });
// Count static QR codes // Count static QR codes
const staticQRCount = await db.qRCode.count({ const staticQRCount = await db.qRCode.count({
where: { where: {
userId, userId,
type: 'STATIC', type: 'STATIC',
}, },
}); });
// Determine limits based on plan // Determine limits based on plan
let dynamicLimit = 3; // FREE plan default let dynamicLimit = 3; // FREE plan default
if (user.plan === 'PRO') { if (user.plan === 'PRO') {
dynamicLimit = 50; dynamicLimit = 50;
} else if (user.plan === 'BUSINESS') { } else if (user.plan === 'BUSINESS') {
dynamicLimit = 500; dynamicLimit = 500;
} } else if ((user.plan as string) === 'ENTERPRISE') {
dynamicLimit = 99999;
return NextResponse.json({ }
dynamicUsed: dynamicQRCount,
dynamicLimit, return NextResponse.json({
staticUsed: staticQRCount, dynamicUsed: dynamicQRCount,
}); dynamicLimit,
} catch (error) { staticUsed: staticQRCount,
console.error('Error fetching user stats:', error); });
return NextResponse.json( } catch (error) {
{ error: 'Internal server error' }, console.error('Error fetching user stats:', error);
{ status: 500 } return NextResponse.json(
); { error: 'Internal server error' },
} { status: 500 }
} );
}
}

View File

@@ -87,6 +87,10 @@ export async function GET(
const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050'; const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
destination = `${baseUrlFeedback}/feedback/${slug}`; destination = `${baseUrlFeedback}/feedback/${slug}`;
break; break;
case 'BARCODE':
// Dynamic barcode redirects to its stored URL
destination = ensureAbsoluteUrl(content.url || content.value || 'https://example.com');
break;
default: default:
destination = 'https://example.com'; destination = 'https://example.com';
} }

View File

@@ -206,6 +206,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly', changeFrequency: 'monthly',
priority: 0.9, priority: 0.9,
}, },
{
url: `${baseUrl}/dynamic-barcode-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{ {
url: `${baseUrl}/bulk-qr-code-generator`, url: `${baseUrl}/bulk-qr-code-generator`,
lastModified: new Date(), lastModified: new Date(),

View File

@@ -1,141 +1,164 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { BillingToggle } from '@/components/ui/BillingToggle'; import { BillingToggle } from '@/components/ui/BillingToggle';
interface PricingProps { interface PricingProps {
t: any; // i18n translation function t: any; // i18n translation function
} }
export const Pricing: React.FC<PricingProps> = ({ t }) => { export const Pricing: React.FC<PricingProps> = ({ t }) => {
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month'); const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
const plans = [ const plans = [
{ {
key: 'free', key: 'free',
popular: false, popular: false,
}, },
{ {
key: 'pro', key: 'pro',
popular: true, popular: true,
}, },
{ {
key: 'business', key: 'business',
popular: false, popular: false,
}, },
]; {
key: 'enterprise',
return ( popular: false,
<section id="pricing" className="py-16"> },
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> ];
<motion.div
initial={{ opacity: 0, y: 20 }} return (
whileInView={{ opacity: 1, y: 0 }} <section id="pricing" className="py-16">
viewport={{ once: true }} <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
transition={{ duration: 0.5 }} <motion.div
className="text-center mb-12" initial={{ opacity: 0, y: 20 }}
> whileInView={{ opacity: 1, y: 0 }}
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4"> viewport={{ once: true }}
{t.pricing.title} transition={{ duration: 0.5 }}
</h2> className="text-center mb-12"
<p className="text-xl text-gray-600"> >
{t.pricing.subtitle} <h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
</p> {t.pricing.title}
</motion.div> </h2>
<p className="text-xl text-gray-600">{t.pricing.subtitle}</p>
<div className="flex justify-center mb-8"> </motion.div>
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div> <div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto"> </div>
{plans.map((plan, index) => (
<motion.div <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
key={plan.key} {plans.map((plan, index) => (
initial={{ opacity: 0, y: 20 }} <motion.div
whileInView={{ opacity: 1, y: 0 }} key={plan.key}
viewport={{ once: true }} initial={{ opacity: 0, y: 20 }}
transition={{ duration: 0.5, delay: index * 0.1 }} whileInView={{ opacity: 1, y: 0 }}
className="h-full" viewport={{ once: true }}
> transition={{ duration: 0.5, delay: index * 0.1 }}
<Card className="h-full"
className={`h-full flex flex-col ${plan.popular >
? 'border-primary-500 shadow-xl relative scale-105 z-10' <Card
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all' className={`h-full flex flex-col ${
}`} plan.popular
> ? 'border-primary-500 shadow-xl relative scale-105 z-10'
{plan.popular && ( : 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center"> }`}
<Badge variant="info" className="px-4 py-1.5 shadow-sm"> >
{t.pricing[plan.key].badge} {plan.popular && (
</Badge> <div className="absolute -top-4 left-1/2 transform -translate-x-1/2 w-full text-center">
</div> <Badge variant="info" className="px-4 py-1.5 shadow-sm">
)} {t.pricing[plan.key].badge}
</Badge>
<CardHeader className="text-center pb-8"> </div>
<CardTitle className="text-2xl mb-4"> )}
{t.pricing[plan.key].title}
</CardTitle> <CardHeader className="text-center pb-8">
<div className="flex flex-col items-center"> <CardTitle className="text-2xl mb-4">
<div className="flex items-baseline justify-center"> {t.pricing[plan.key].title}
<span className="text-4xl font-bold"> </CardTitle>
{plan.key === 'free' <div className="flex flex-col items-center">
? t.pricing[plan.key].price <div className="flex items-baseline justify-center">
: billingPeriod === 'month' <span className="text-4xl font-bold">
? t.pricing[plan.key].price {plan.key === 'free' || plan.key === 'enterprise'
: plan.key === 'pro' ? t.pricing[plan.key].price
? '€90' : billingPeriod === 'month'
: '€290'} ? t.pricing[plan.key].price
</span> : plan.key === 'pro'
<span className="text-gray-600 ml-2"> ? '€90'
{plan.key === 'free' : '€290'}
? t.pricing[plan.key].period </span>
: billingPeriod === 'month' <span className="text-gray-600 ml-2">
? t.pricing[plan.key].period {plan.key === 'free' || plan.key === 'enterprise'
: 'per year'} ? t.pricing[plan.key].period
</span> : billingPeriod === 'month'
</div> ? t.pricing[plan.key].period
{billingPeriod === 'year' && plan.key !== 'free' && ( : 'per year'}
<Badge variant="success" className="mt-2"> </span>
Save 16% </div>
</Badge> {billingPeriod === 'year' &&
)} plan.key !== 'free' &&
</div> plan.key !== 'enterprise' && (
</CardHeader> <Badge variant="success" className="mt-2">
Save 16%
<CardContent className="space-y-8 flex-1 flex flex-col"> </Badge>
<ul className="space-y-3 flex-1"> )}
{t.pricing[plan.key].features.map((feature: string, index: number) => ( </div>
<li key={index} className="flex items-start space-x-3"> </CardHeader>
<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" /> <CardContent className="space-y-8 flex-1 flex flex-col">
</svg> <ul className="space-y-3 flex-1">
<span className="text-gray-700">{feature}</span> {t.pricing[plan.key].features.map(
</li> (feature: string, index: number) => (
))} <li key={index} className="flex items-start space-x-3">
</ul> <svg
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
<div className="mt-8 pt-8 border-t border-gray-100"> fill="currentColor"
<Link href="/signup"> viewBox="0 0 20 20"
<Button >
variant={plan.popular ? 'primary' : 'outline'} <path
className="w-full" fillRule="evenodd"
size="lg" 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"
Get Started />
</Button> </svg>
</Link> <span className="text-gray-700">{feature}</span>
</div> </li>
</CardContent> )
</Card> )}
</motion.div> </ul>
))}
</div> <div className="mt-8 pt-8 border-t border-gray-100">
</div> {plan.key === 'enterprise' ? (
</section> <Link href="mailto:timo@qrmaster.net">
); <Button variant="outline" className="w-full" size="lg">
}; {t.pricing[plan.key].contact || 'Contact Us'}
</Button>
</Link>
) : (
<Link href="/signup">
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>
Get Started
</Button>
</Link>
)}
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
};

View File

@@ -19,10 +19,10 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
<div> <div>
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity"> <Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity">
<img src="/favicon1.png" alt="QR Master Logo" className="w-[68px] h-[68px] rounded-full object-cover" /> <img src="/favicon1.png" alt="QR Master Logo" className="w-[68px] h-[68px] rounded-full object-cover" />
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span> <span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
</Link> </Link>
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}> <p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
{translations.tagline} {translations.tagline}
</p> </p>
@@ -64,6 +64,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<li><Link href="/faq" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_questions}</Link></li> <li><Link href="/faq" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_questions}</Link></li>
<li><Link href="/blog" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_articles}</Link></li> <li><Link href="/blog" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_articles}</Link></li>
<li><Link href="/bulk-qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bulk QR Generator</Link></li> <li><Link href="/bulk-qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bulk QR Generator</Link></li>
<li><Link href="/dynamic-barcode-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Dynamic Barcode Generator</Link></li>
<li> <li>
<Link href="/qr-code-for" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}> <Link href="/qr-code-for" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>
@@ -100,14 +101,14 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
</ul> </ul>
</div> </div>
<div> <div>
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3> <h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3>
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}> <ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
<li><Link href="/alternatives" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Alternatives</Link></li> <li><Link href="/alternatives" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Alternatives</Link></li>
<li><Link href="/vs" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Comparisons</Link></li> <li><Link href="/vs" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>All Comparisons</Link></li>
<li><Link href="/alternatives/qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR-Code-Generator Alt.</Link></li> <li><Link href="/alternatives/qr-code-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR-Code-Generator Alt.</Link></li>
<li><Link href="/alternatives/flowcode" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Flowcode Alternative</Link></li> <li><Link href="/alternatives/flowcode" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Flowcode Alternative</Link></li>
<li><Link href="/alternatives/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Beaconstac Alternative</Link></li> <li><Link href="/alternatives/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Beaconstac Alternative</Link></li>
<li><Link href="/alternatives/bitly" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bitly Alternative</Link></li> <li><Link href="/alternatives/bitly" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Bitly Alternative</Link></li>
<li><Link href="/vs/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Master vs Uniqode</Link></li> <li><Link href="/vs/beaconstac" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Master vs Uniqode</Link></li>
</ul> </ul>
@@ -136,7 +137,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
) : ( ) : (
<div></div> <div></div>
)} )}
<p>&copy; 2026 {translations.rights_reserved}</p> <p>&copy; 2026 {translations.rights_reserved}</p>
<div className="w-12"></div> <div className="w-12"></div>
</div> </div>
</div> </div>

View File

@@ -1,408 +1,421 @@
{ {
"nav": { "nav": {
"features": "Funktionen", "features": "Funktionen",
"pricing": "Preise", "pricing": "Preise",
"faq": "FAQ", "faq": "FAQ",
"blog": "Blog", "blog": "Blog",
"login": "Anmelden", "login": "Anmelden",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"about": "Über uns", "about": "Über uns",
"contact": "Kontakt", "contact": "Kontakt",
"signup": "Registrieren", "signup": "Registrieren",
"learn": "Lernen", "learn": "Lernen",
"create_qr": "QR erstellen", "create_qr": "QR erstellen",
"bulk_creation": "Massen-Erstellung", "bulk_creation": "Massen-Erstellung",
"analytics": "Analytik", "analytics": "Analytik",
"settings": "Einstellungen", "settings": "Einstellungen",
"cta": "Kostenlos starten", "cta": "Kostenlos starten",
"tools": "Kostenlose Tools", "tools": "Kostenlose Tools",
"all_free": "Alle Generatoren sind 100% kostenlos", "all_free": "Alle Generatoren sind 100% kostenlos",
"resources": "Ressourcen", "resources": "Ressourcen",
"all_industries": "Branchen" "all_industries": "Branchen"
}, },
"hero": { "hero": {
"badge": "Kostenloser QR-Code-Generator", "badge": "Kostenloser QR-Code-Generator",
"title": "Erstellen Sie QR-Codes, die überall funktionieren", "title": "Erstellen Sie QR-Codes, die überall funktionieren",
"subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos für immer.", "subtitle": "Generieren Sie statische und dynamische QR-Codes mit Tracking, individuellem Branding und Massen-Erstellung. Kostenlos für immer.",
"features": [ "features": [
"Keine Kreditkarte zum Starten erforderlich", "Keine Kreditkarte zum Starten erforderlich",
"QR-Codes für immer kostenlos erstellen", "QR-Codes für immer kostenlos erstellen",
"Erweiterte Verfolgung und Analytik", "Erweiterte Verfolgung und Analytik",
"Individuelle Farben und Stile" "Individuelle Farben und Stile"
], ],
"cta_primary": "QR-Code kostenlos erstellen", "cta_primary": "QR-Code kostenlos erstellen",
"cta_secondary": "Preise ansehen", "cta_secondary": "Preise ansehen",
"engagement_badge": "Kostenlos für immer", "engagement_badge": "Kostenlos für immer",
"get_started": "Loslegen", "get_started": "Loslegen",
"view_full_pricing": "Alle Preisdetails ansehen →" "view_full_pricing": "Alle Preisdetails ansehen →"
}, },
"trust": { "trust": {
"users": "Aktive Nutzer", "users": "Aktive Nutzer",
"codes": "QR-Codes erstellt", "codes": "QR-Codes erstellt",
"scans": "Scans verfolgt", "scans": "Scans verfolgt",
"countries": "Länder" "countries": "Länder"
}, },
"industries": { "industries": {
"restaurant": "Restaurant-Kette", "restaurant": "Restaurant-Kette",
"tech": "Tech-Startup", "tech": "Tech-Startup",
"realestate": "Immobilien", "realestate": "Immobilien",
"events": "Event-Agentur", "events": "Event-Agentur",
"retail": "Einzelhandel", "retail": "Einzelhandel",
"healthcare": "Gesundheitswesen" "healthcare": "Gesundheitswesen"
}, },
"templates": { "templates": {
"title": "Mit einer Vorlage beginnen", "title": "Mit einer Vorlage beginnen",
"restaurant": "Restaurant-Menü", "restaurant": "Restaurant-Menü",
"business": "Visitenkarte", "business": "Visitenkarte",
"vcard": "Kontaktkarte", "vcard": "Kontaktkarte",
"event": "Event-Ticket", "event": "Event-Ticket",
"use_template": "Vorlage verwenden →" "use_template": "Vorlage verwenden →"
}, },
"generator": { "generator": {
"title": "Sofortiger QR-Code-Generator", "title": "Sofortiger QR-Code-Generator",
"url_placeholder": "Geben Sie hier Ihre URL ein...", "url_placeholder": "Geben Sie hier Ihre URL ein...",
"foreground": "Vordergrund", "foreground": "Vordergrund",
"background": "Hintergrund", "background": "Hintergrund",
"corners": "Ecken", "corners": "Ecken",
"size": "Größe", "size": "Größe",
"contrast_good": "Guter Kontrast", "contrast_good": "Guter Kontrast",
"download_svg": "SVG herunterladen", "download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen", "download_png": "PNG herunterladen",
"save_track": "Speichern & Verfolgen", "save_track": "Speichern & Verfolgen",
"live_preview": "Live-Vorschau", "live_preview": "Live-Vorschau",
"demo_note": "Dies ist ein Demo-QR-Code" "demo_note": "Dies ist ein Demo-QR-Code"
}, },
"static_vs_dynamic": { "static_vs_dynamic": {
"title": "Warum dynamische QR-Codes Ihnen Geld sparen", "title": "Warum dynamische QR-Codes Ihnen Geld sparen",
"description": "Hören Sie auf, Materialien neu zu drucken. Wechseln Sie Ziele sofort und verfolgen Sie jeden Scan.", "description": "Hören Sie auf, Materialien neu zu drucken. Wechseln Sie Ziele sofort und verfolgen Sie jeden Scan.",
"static": { "static": {
"title": "Statische QR-Codes", "title": "Statische QR-Codes",
"subtitle": "Immer kostenlos", "subtitle": "Immer kostenlos",
"description": "Perfekt für permanente Inhalte, die sich nie ändern", "description": "Perfekt für permanente Inhalte, die sich nie ändern",
"features": [ "features": [
"Inhalt kann nicht bearbeitet werden", "Inhalt kann nicht bearbeitet werden",
"Keine Scan-Verfolgung", "Keine Scan-Verfolgung",
"Funktioniert für immer", "Funktioniert für immer",
"Kein Konto erforderlich" "Kein Konto erforderlich"
] ]
}, },
"dynamic": { "dynamic": {
"title": "Dynamische QR-Codes", "title": "Dynamische QR-Codes",
"subtitle": "Empfohlen", "subtitle": "Empfohlen",
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen", "description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
"features": [ "features": [
"Inhalt jederzeit bearbeiten", "Inhalt jederzeit bearbeiten",
"Erweiterte Analytik", "Erweiterte Analytik",
"Individuelles Branding", "Individuelles Branding",
"Bulk-Operationen" "Bulk-Operationen"
] ]
} }
}, },
"features": { "features": {
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen", "title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
"analytics": { "analytics": {
"title": "Erweiterte Analytik", "title": "Erweiterte Analytik",
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken." "description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
}, },
"customization": { "customization": {
"title": "Vollständige Anpassung", "title": "Vollständige Anpassung",
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen." "description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
}, },
"unlimited": { "unlimited": {
"title": "Unbegrenzte statische QR-Codes", "title": "Unbegrenzte statische QR-Codes",
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits." "description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
}, },
"bulk": { "bulk": {
"title": "Bulk-Operationen", "title": "Bulk-Operationen",
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung." "description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
}, },
"integrations": { "integrations": {
"title": "Integrationen", "title": "Integrationen",
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools." "description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
}, },
"api": { "api": {
"title": "Entwickler-API", "title": "Entwickler-API",
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API." "description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
}, },
"support": { "support": {
"title": "24/7 Support", "title": "24/7 Support",
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team." "description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
} }
}, },
"pricing": { "pricing": {
"title": "Wählen Sie Ihren Plan", "title": "Wählen Sie Ihren Plan",
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse", "subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"choose_plan": "Wählen Sie Ihren Plan", "choose_plan": "Wählen Sie Ihren Plan",
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse", "select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
"current_plan": "Aktueller Plan", "current_plan": "Aktueller Plan",
"upgrade_to": "Upgrade auf", "upgrade_to": "Upgrade auf",
"downgrade_to_free": "Zu Kostenlos zurückstufen", "downgrade_to_free": "Zu Kostenlos zurückstufen",
"most_popular": "Beliebteste", "most_popular": "Beliebteste",
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.", "all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
"free": { "free": {
"title": "Kostenlos", "title": "Kostenlos",
"name": "Free", "name": "Free",
"price": "€0", "price": "€0",
"period": "für immer", "period": "für immer",
"features": [ "features": [
"3 aktive dynamische QR-Codes (8 Typen verfügbar)", "3 aktive dynamische QR-Codes (8 Typen verfügbar)",
"Unbegrenzte statische QR-Codes", "Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking", "Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen" "Standard QR-Design-Vorlagen"
] ]
}, },
"pro": { "pro": {
"title": "Pro", "title": "Pro",
"name": "Pro", "name": "Pro",
"price": "€9", "price": "€9",
"period": "pro Monat", "period": "pro Monat",
"badge": "Beliebteste", "badge": "Beliebteste",
"features": [ "features": [
"50 dynamische QR-Codes", "50 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes", "Unbegrenzte statische QR-Codes",
"Erweiterte Analytik (Scans, Geräte, Standorte)", "Erweiterte Analytik (Scans, Geräte, Standorte)",
"Individuelles Branding (Farben)", "Individuelles Branding (Farben)",
"Download als SVG/PNG" "Download als SVG/PNG"
] ]
}, },
"business": { "business": {
"title": "Business", "title": "Business",
"name": "Business", "name": "Business",
"price": "€29", "price": "€29",
"period": "pro Monat", "period": "pro Monat",
"features": [ "features": [
"500 dynamische QR-Codes", "500 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes", "Unbegrenzte statische QR-Codes",
"Alles aus Pro", "Alles aus Pro",
"Massen-QR-Erstellung (bis zu 1.000)", "Massen-QR-Erstellung (bis zu 1.000)",
"Prioritäts-E-Mail-Support", "Prioritäts-E-Mail-Support",
"Erweiterte Tracking & Insights" "Erweiterte Tracking & Insights"
] ]
} },
}, "enterprise": {
"faq": { "title": "Enterprise",
"title": "Häufig gestellte Fragen", "name": "Enterprise",
"questions": { "price": "Individuell",
"account": { "period": "",
"question": "Benötige ich ein Konto, um QR-Codes zu erstellen?", "features": [
"answer": "Für statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto." "∞ dynamische QR-Codes",
}, "Unbegrenzte statische QR-Codes",
"static_vs_dynamic": { "Alles aus Business",
"question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?", "Eigener Account Manager"
"answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geändert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik." ],
}, "contact": "Kontakt aufnehmen"
"forever": { }
"question": "Funktionieren meine QR-Codes für immer?", },
"answer": "Statische QR-Codes funktionieren für immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist." "faq": {
}, "title": "Häufig gestellte Fragen",
"file_type": { "questions": {
"question": "Welchen Dateityp sollte ich zum Drucken verwenden?", "account": {
"answer": "Für Druckmaterialien empfehlen wir das SVG-Format für Skalierbarkeit oder hochauflösendes PNG (300+ DPI) für beste Qualität." "question": "Benötige ich ein Konto, um QR-Codes zu erstellen?",
}, "answer": "Für statische QR-Codes ist kein Konto erforderlich. Dynamische QR-Codes mit Tracking- und Bearbeitungsfunktionen erfordern jedoch ein kostenloses Konto."
"password": { },
"question": "Kann ich einen QR-Code mit einem Passwort schützen?", "static_vs_dynamic": {
"answer": "Ja, Pro- und Business-Pläne beinhalten Passwortschutz und Zugriffskontrollfunktionen für Ihre QR-Codes." "question": "Was ist der Unterschied zwischen statischen und dynamischen QR-Codes?",
}, "answer": "Statische QR-Codes enthalten feste Inhalte, die nicht geändert werden können. Dynamische QR-Codes können jederzeit bearbeitet werden und bieten detaillierte Analytik."
"analytics": { },
"question": "Wie funktioniert die Analytik?", "forever": {
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert." "question": "Funktionieren meine QR-Codes für immer?",
}, "answer": "Statische QR-Codes funktionieren für immer, da der Inhalt direkt eingebettet ist. Dynamische QR-Codes funktionieren, solange Ihr Konto aktiv ist."
"privacy": { },
"question": "Verfolgen Sie persönliche Daten?", "file_type": {
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header." "question": "Welchen Dateityp sollte ich zum Drucken verwenden?",
}, "answer": "Für Druckmaterialien empfehlen wir das SVG-Format für Skalierbarkeit oder hochauflösendes PNG (300+ DPI) für beste Qualität."
"bulk": { },
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?", "password": {
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen." "question": "Kann ich einen QR-Code mit einem Passwort schützen?",
} "answer": "Ja, Pro- und Business-Pläne beinhalten Passwortschutz und Zugriffskontrollfunktionen für Ihre QR-Codes."
} },
}, "analytics": {
"dashboard": { "question": "Wie funktioniert die Analytik?",
"title": "Dashboard", "answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance", },
"stats": { "privacy": {
"total_scans": "Gesamte Scans", "question": "Verfolgen Sie persönliche Daten?",
"active_codes": "Aktive QR-Codes", "answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
"conversion_rate": "Konversionsrate" },
}, "bulk": {
"recent_codes": "Aktuelle QR-Codes", "question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
"blog_resources": "Blog & Ressourcen", "answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
"menu": { }
"edit": "Bearbeiten", }
"duplicate": "Duplizieren", },
"pause": "Pausieren", "dashboard": {
"delete": "Löschen" "title": "Dashboard",
} "subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
}, "stats": {
"create": { "total_scans": "Gesamte Scans",
"title": "QR-Code erstellen", "active_codes": "Aktive QR-Codes",
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding", "conversion_rate": "Konversionsrate"
"content": "Inhalt", },
"type": "QR-Code-Typ", "recent_codes": "Aktuelle QR-Codes",
"style": "Stil & Branding", "blog_resources": "Blog & Ressourcen",
"preview": "Live-Vorschau", "menu": {
"title_label": "Titel", "edit": "Bearbeiten",
"title_placeholder": "Mein QR-Code", "duplicate": "Duplizieren",
"content_type": "Inhaltstyp", "pause": "Pausieren",
"url_label": "URL", "delete": "Löschen"
"url_placeholder": "https://beispiel.de", }
"tags_label": "Tags (durch Komma getrennt)", },
"tags_placeholder": "marketing, kampagne, 2025", "create": {
"qr_code_type": "QR-Code-Typ", "title": "QR-Code erstellen",
"dynamic": "Dynamisch", "subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
"static": "Statisch", "content": "Inhalt",
"recommended": "Empfohlen", "type": "QR-Code-Typ",
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.", "style": "Stil & Branding",
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.", "preview": "Live-Vorschau",
"foreground_color": "Vordergrundfarbe", "title_label": "Titel",
"background_color": "Hintergrundfarbe", "title_placeholder": "Mein QR-Code",
"corner_style": "Eckenstil", "content_type": "Inhaltstyp",
"size": "Größe", "url_label": "URL",
"good_contrast": "Guter Kontrast", "url_placeholder": "https://beispiel.de",
"contrast_ratio": "Kontrastverhältnis", "tags_label": "Tags (durch Komma getrennt)",
"download_svg": "SVG herunterladen", "tags_placeholder": "marketing, kampagne, 2025",
"download_png": "PNG herunterladen", "qr_code_type": "QR-Code-Typ",
"save_qr_code": "QR-Code speichern" "dynamic": "Dynamisch",
}, "static": "Statisch",
"analytics": { "recommended": "Empfohlen",
"title": "Analytik", "dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes", "static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
"export_report": "Bericht exportieren", "foreground_color": "Vordergrundfarbe",
"from_last_period": "vom letzten Zeitraum", "background_color": "Hintergrundfarbe",
"no_mobile_scans": "Keine mobilen Scans", "corner_style": "Eckenstil",
"of_total": "der Gesamtmenge", "size": "Größe",
"ranges": { "good_contrast": "Guter Kontrast",
"7d": "7 Tage", "contrast_ratio": "Kontrastverhältnis",
"30d": "30 Tage", "download_svg": "SVG herunterladen",
"90d": "90 Tage" "download_png": "PNG herunterladen",
}, "save_qr_code": "QR-Code speichern"
"kpis": { },
"total_scans": "Gesamte Scans", "analytics": {
"avg_scans": "Ø Scans/QR", "title": "Analytik",
"mobile_usage": "Mobile Nutzung", "subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
"top_country": "Top Land" "export_report": "Bericht exportieren",
}, "from_last_period": "vom letzten Zeitraum",
"charts": { "no_mobile_scans": "Keine mobilen Scans",
"scans_over_time": "Scans über Zeit", "of_total": "der Gesamtmenge",
"device_types": "Gerätetypen", "ranges": {
"top_countries": "Top Länder" "7d": "7 Tage",
}, "30d": "30 Tage",
"table": { "90d": "90 Tage"
"qr_code": "QR-Code", },
"type": "Typ", "kpis": {
"total_scans": "Gesamte Scans", "total_scans": "Gesamte Scans",
"unique_scans": "Einzigartige Scans", "avg_scans": "Ø Scans/QR",
"conversion": "Konversion", "mobile_usage": "Mobile Nutzung",
"trend": "Trend", "top_country": "Top Land"
"scans": "Scans", },
"percentage": "Prozent", "charts": {
"country": "Land", "scans_over_time": "Scans über Zeit",
"performance": "Performance", "device_types": "Gerätetypen",
"created": "Erstellt", "top_countries": "Top Länder"
"status": "Status" },
}, "table": {
"performance_title": "QR-Code-Performance" "qr_code": "QR-Code",
}, "type": "Typ",
"bulk": { "total_scans": "Gesamte Scans",
"title": "Massen-Erstellung", "unique_scans": "Einzigartige Scans",
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien", "conversion": "Konversion",
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat", "trend": "Trend",
"template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten für Titel und Inhalt (URL) enthalten.", "scans": "Scans",
"static_only_title": "Nur statische QR-Codes", "percentage": "Prozent",
"static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt für Druckmaterialien und Offline-Nutzung.", "country": "Land",
"download_template": "Vorlage herunterladen", "performance": "Performance",
"no_file_selected": "Keine ausgewählt", "created": "Erstellt",
"simple_format": "Einfaches Format", "status": "Status"
"just_title_url": "Nur Titel & URL", },
"static_qr_codes": "Statische QR-Codes", "performance_title": "QR-Code-Performance"
"no_tracking": "Kein Tracking enthalten", },
"instant_download": "Sofortiger Download", "bulk": {
"get_zip": "ZIP mit allen SVGs erhalten", "title": "Massen-Erstellung",
"max_rows": "max 1.000 Zeilen", "subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
"steps": { "template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
"upload": "Datei hochladen", "template_warning_text": "Laden Sie die Vorlage unten herunter und folgen Sie dem Format genau. Ihre CSV muss Spalten für Titel und Inhalt (URL) enthalten.",
"preview": "Vorschau & Zuordnung", "static_only_title": "Nur statische QR-Codes",
"download": "Herunterladen" "static_only_text": "Massen-Erstellung generiert statische QR-Codes, die nach der Erstellung nicht bearbeitet werden können. Diese QR-Codes beinhalten kein Tracking oder Analytik. Perfekt für Druckmaterialien und Offline-Nutzung.",
}, "download_template": "Vorlage herunterladen",
"drag_drop": "Datei hier hinziehen", "no_file_selected": "Keine ausgewählt",
"or_click": "oder klicken zum Durchsuchen", "simple_format": "Einfaches Format",
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)" "just_title_url": "Nur Titel & URL",
}, "static_qr_codes": "Statische QR-Codes",
"integrations": { "no_tracking": "Kein Tracking enthalten",
"title": "Integrationen", "instant_download": "Sofortiger Download",
"metrics": { "get_zip": "ZIP mit allen SVGs erhalten",
"total_codes": "QR-Codes Gesamt", "max_rows": "max 1.000 Zeilen",
"active_integrations": "Aktive Integrationen", "steps": {
"sync_status": "Sync-Status", "upload": "Datei hochladen",
"available_services": "Verfügbare Services" "preview": "Vorschau & Zuordnung",
}, "download": "Herunterladen"
"zapier": { },
"title": "Zapier", "drag_drop": "Datei hier hinziehen",
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps", "or_click": "oder klicken zum Durchsuchen",
"features": [ "supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
"Trigger bei neuen QR-Codes", },
"Codes aus anderen Apps erstellen", "integrations": {
"Scan-Daten synchronisieren" "title": "Integrationen",
] "metrics": {
}, "total_codes": "QR-Codes Gesamt",
"airtable": { "active_integrations": "Aktive Integrationen",
"title": "Airtable", "sync_status": "Sync-Status",
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen", "available_services": "Verfügbare Services"
"features": [ },
"Bidirektionale Synchronisation", "zapier": {
"Individuelle Feldzuordnung", "title": "Zapier",
"Echtzeit-Updates" "description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
] "features": [
}, "Trigger bei neuen QR-Codes",
"sheets": { "Codes aus anderen Apps erstellen",
"title": "Google Sheets", "Scan-Daten synchronisieren"
"description": "Exportieren Sie Daten automatisch zu Google Sheets", ]
"features": [ },
"Automatisierte Exporte", "airtable": {
"Individuelle Vorlagen", "title": "Airtable",
"Geplante Updates" "description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
] "features": [
}, "Bidirektionale Synchronisation",
"activate": "Aktivieren & Konfigurieren" "Individuelle Feldzuordnung",
}, "Echtzeit-Updates"
"settings": { ]
"title": "Einstellungen", },
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen", "sheets": {
"tabs": { "title": "Google Sheets",
"profile": "Profil", "description": "Exportieren Sie Daten automatisch zu Google Sheets",
"billing": "Abrechnung", "features": [
"team": "Team & Rollen", "Automatisierte Exporte",
"api": "API-Schlüssel", "Individuelle Vorlagen",
"workspace": "Arbeitsbereich" "Geplante Updates"
} ]
}, },
"common": { "activate": "Aktivieren & Konfigurieren"
"save": "Speichern", },
"cancel": "Abbrechen", "settings": {
"delete": "Löschen", "title": "Einstellungen",
"edit": "Bearbeiten", "subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
"create": "Erstellen", "tabs": {
"loading": "Lädt...", "profile": "Profil",
"error": "Ein Fehler ist aufgetreten", "billing": "Abrechnung",
"success": "Erfolgreich!" "team": "Team & Rollen",
}, "api": "API-Schlüssel",
"footer": { "workspace": "Arbeitsbereich"
"product": "Produkt", }
"features": "Funktionen", },
"pricing": "Preise", "common": {
"faq": "FAQ", "save": "Speichern",
"blog": "Blog", "cancel": "Abbrechen",
"resources": "Ressourcen", "delete": "Löschen",
"full_pricing": "Alle Preise", "edit": "Bearbeiten",
"all_questions": "Alle Fragen", "create": "Erstellen",
"all_articles": "Alle Artikel", "loading": "Lädt...",
"learn": "Lernen", "error": "Ein Fehler ist aufgetreten",
"success": "Erfolgreich!"
},
"footer": {
"product": "Produkt",
"features": "Funktionen",
"pricing": "Preise",
"faq": "FAQ",
"blog": "Blog",
"resources": "Ressourcen",
"full_pricing": "Alle Preise",
"all_questions": "Alle Fragen",
"all_articles": "Alle Artikel",
"learn": "Lernen",
"get_started": "Loslegen", "get_started": "Loslegen",
"legal": "Rechtliches", "legal": "Rechtliches",
"industries": "Branchen", "industries": "Branchen",
"privacy_policy": "Datenschutzerklärung", "privacy_policy": "Datenschutzerklärung",
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.", "tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
"newsletter": "Newsletter-Anmeldung", "newsletter": "Newsletter-Anmeldung",
"rights_reserved": "QR Master. Alle Rechte vorbehalten." "rights_reserved": "QR Master. Alle Rechte vorbehalten."
} }
} }

View File

@@ -1,406 +1,411 @@
{ {
"nav": { "nav": {
"features": "Features", "features": "Features",
"pricing": "Pricing", "pricing": "Pricing",
"faq": "FAQ", "faq": "FAQ",
"blog": "Blog", "blog": "Blog",
"login": "Login", "login": "Login",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"about": "About", "about": "About",
"contact": "Contact", "contact": "Contact",
"signup": "Sign Up", "signup": "Sign Up",
"learn": "Learn", "learn": "Learn",
"create_qr": "Create QR", "create_qr": "Create QR",
"bulk_creation": "Bulk Creation", "bulk_creation": "Bulk Creation",
"analytics": "Analytics", "analytics": "Analytics",
"settings": "Settings", "settings": "Settings",
"cta": "Get Started Free", "cta": "Get Started Free",
"tools": "Free Tools", "tools": "Free Tools",
"all_free": "All generators are 100% free", "all_free": "All generators are 100% free",
"resources": "Resources", "resources": "Resources",
"all_industries": "Industries" "all_industries": "Industries"
}, },
"hero": { "hero": {
"badge": "Free QR Code Generator", "badge": "Free QR Code Generator",
"title": "Create QR Codes That Work Everywhere", "title": "Create QR Codes That Work Everywhere",
"subtitle": "Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever.", "subtitle": "Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever.",
"features": [ "features": [
"No credit card required to start", "No credit card required to start",
"Create QR codes free forever", "Create QR codes free forever",
"Advanced tracking and analytics", "Advanced tracking and analytics",
"Custom colors and styles" "Custom colors and styles"
], ],
"cta_primary": "Make a QR Code Free", "cta_primary": "Make a QR Code Free",
"cta_secondary": "View Pricing", "cta_secondary": "View Pricing",
"engagement_badge": "Free Forever" "engagement_badge": "Free Forever"
}, },
"trust": { "trust": {
"users": "Happy Users", "users": "Happy Users",
"codes": "Active QR Codes", "codes": "Active QR Codes",
"scans": "Total Scans", "scans": "Total Scans",
"countries": "Countries" "countries": "Countries"
}, },
"industries": { "industries": {
"restaurant": "Restaurant Chain", "restaurant": "Restaurant Chain",
"tech": "Tech Startup", "tech": "Tech Startup",
"realestate": "Real Estate", "realestate": "Real Estate",
"events": "Event Agency", "events": "Event Agency",
"retail": "Retail Store", "retail": "Retail Store",
"healthcare": "Healthcare" "healthcare": "Healthcare"
}, },
"templates": { "templates": {
"title": "Start with a Template", "title": "Start with a Template",
"restaurant": "Restaurant Menu", "restaurant": "Restaurant Menu",
"business": "Business Card", "business": "Business Card",
"vcard": "Contact Card", "vcard": "Contact Card",
"event": "Event Ticket", "event": "Event Ticket",
"use_template": "Use template →" "use_template": "Use template →"
}, },
"generator": { "generator": {
"title": "Instant QR Code Generator", "title": "Instant QR Code Generator",
"url_placeholder": "Enter your URL here...", "url_placeholder": "Enter your URL here...",
"foreground": "Foreground", "foreground": "Foreground",
"background": "Background", "background": "Background",
"corners": "Corners", "corners": "Corners",
"size": "Size", "size": "Size",
"contrast_good": "Good contrast", "contrast_good": "Good contrast",
"download_svg": "Download SVG", "download_svg": "Download SVG",
"download_png": "Download PNG", "download_png": "Download PNG",
"save_track": "Save & Track", "save_track": "Save & Track",
"live_preview": "Live Preview", "live_preview": "Live Preview",
"demo_note": "This is a demo QR code" "demo_note": "This is a demo QR code"
}, },
"static_vs_dynamic": { "static_vs_dynamic": {
"title": "Why Dynamic QR Codes Save You Money", "title": "Why Dynamic QR Codes Save You Money",
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.", "description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
"static": { "static": {
"title": "Static QR Codes", "title": "Static QR Codes",
"subtitle": "Always Free", "subtitle": "Always Free",
"description": "Perfect for permanent content that never changes", "description": "Perfect for permanent content that never changes",
"features": [ "features": [
"Content cannot be edited", "Content cannot be edited",
"No scan tracking", "No scan tracking",
"Works forever", "Works forever",
"No account required" "No account required"
] ]
}, },
"dynamic": { "dynamic": {
"title": "Dynamic QR Codes", "title": "Dynamic QR Codes",
"subtitle": "Recommended", "subtitle": "Recommended",
"description": "Full control with tracking and editing capabilities", "description": "Full control with tracking and editing capabilities",
"features": [ "features": [
"Edit content anytime", "Edit content anytime",
"Advanced analytics", "Advanced analytics",
"Custom branding", "Custom branding",
"Bulk operations" "Bulk operations"
] ]
} }
}, },
"features": { "features": {
"title": "Everything you need to create professional QR codes", "title": "Everything you need to create professional QR codes",
"analytics": { "analytics": {
"title": "Advanced Analytics", "title": "Advanced Analytics",
"description": "Track scans, locations, devices, and user behavior with detailed insights." "description": "Track scans, locations, devices, and user behavior with detailed insights."
}, },
"customization": { "customization": {
"title": "Full Customization", "title": "Full Customization",
"description": "Brand your QR codes with custom colors and styling options." "description": "Brand your QR codes with custom colors and styling options."
}, },
"unlimited": { "unlimited": {
"title": "Unlimited Static QR Codes", "title": "Unlimited Static QR Codes",
"description": "Create as many static QR codes as you need. Free forever, no limits." "description": "Create as many static QR codes as you need. Free forever, no limits."
}, },
"bulk": { "bulk": {
"title": "Bulk Operations", "title": "Bulk Operations",
"description": "Create hundreds of QR codes at once with CSV import and batch processing." "description": "Create hundreds of QR codes at once with CSV import and batch processing."
}, },
"integrations": { "integrations": {
"title": "Integrations", "title": "Integrations",
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools." "description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
}, },
"api": { "api": {
"title": "Developer API", "title": "Developer API",
"description": "Integrate QR code generation into your applications with our REST API." "description": "Integrate QR code generation into your applications with our REST API."
}, },
"support": { "support": {
"title": "24/7 Support", "title": "24/7 Support",
"description": "Get help when you need it with our dedicated customer support team." "description": "Get help when you need it with our dedicated customer support team."
} }
}, },
"pricing": { "pricing": {
"title": "Choose Your Plan", "title": "Choose Your Plan",
"subtitle": "Select the perfect plan for your QR code needs", "subtitle": "Select the perfect plan for your QR code needs",
"choose_plan": "Choose Your Plan", "choose_plan": "Choose Your Plan",
"select_plan": "Select the perfect plan for your QR code needs", "select_plan": "Select the perfect plan for your QR code needs",
"current_plan": "Current Plan", "current_plan": "Current Plan",
"upgrade_to": "Upgrade to", "upgrade_to": "Upgrade to",
"downgrade_to_free": "Downgrade to Free", "downgrade_to_free": "Downgrade to Free",
"most_popular": "Most Popular", "most_popular": "Most Popular",
"all_plans_note": "All plans include unlimited static QR codes and basic customization.", "all_plans_note": "All plans include unlimited static QR codes and basic customization.",
"free": { "free": {
"title": "Free", "title": "Free",
"name": "Free", "name": "Free",
"price": "€0", "price": "€0",
"period": "forever", "period": "forever",
"features": [ "features": [
"3 active dynamic QR codes (8 types available)", "3 active dynamic QR codes (8 types available)",
"Unlimited static QR codes", "Unlimited static QR codes",
"Basic scan tracking", "Basic scan tracking",
"Standard QR design templates", "Standard QR design templates",
"Download as SVG/PNG" "Download as SVG/PNG"
] ]
}, },
"pro": { "pro": {
"title": "Pro", "title": "Pro",
"name": "Pro", "name": "Pro",
"price": "€9", "price": "€9",
"period": "per month", "period": "per month",
"badge": "Most Popular", "badge": "Most Popular",
"features": [ "features": [
"50 dynamic QR codes", "50 dynamic QR codes",
"Unlimited static QR codes", "Unlimited static QR codes",
"Advanced analytics (scans, devices, locations)", "Advanced analytics (scans, devices, locations)",
"Custom branding (colors & logos)" "Custom branding (colors & logos)"
] ]
}, },
"business": { "business": {
"title": "Business", "title": "Business",
"name": "Business", "name": "Business",
"price": "€29", "price": "€29",
"period": "per month", "period": "per month",
"features": [ "features": [
"500 dynamic QR codes", "500 dynamic QR codes",
"Unlimited static QR codes", "Unlimited static QR codes",
"Everything from Pro", "Everything from Pro",
"Bulk QR Creation (up to 1,000)", "Bulk QR Creation (up to 1,000)",
"Priority email support", "Priority email support",
"Advanced tracking & insights" "Advanced tracking & insights"
] ]
} },
}, "enterprise": {
"faq": { "title": "Enterprise",
"title": "Frequently Asked Questions", "name": "Enterprise",
"questions": { "price": "Custom",
"account": { "period": "",
"question": "Do I need an account to create QR codes?", "features": [
"answer": "No account is required for static QR codes. However, dynamic QR codes with tracking and editing capabilities require a free account." "∞ dynamic QR codes",
}, "Unlimited static QR codes",
"static_vs_dynamic": { "Everything from Business",
"question": "What's the difference between static and dynamic QR codes?", "Dedicated Account Manager"
"answer": "Static QR codes contain fixed content that cannot be changed. Dynamic QR codes can be edited anytime and provide detailed analytics." ],
}, "contact": "Contact Us"
"forever": { }
"question": "Will my QR codes work forever?", },
"answer": "Static QR codes work forever as the content is embedded directly. Dynamic QR codes work as long as your account is active." "faq": {
}, "title": "Frequently Asked Questions",
"file_type": { "questions": {
"question": "What file type should I use for printing?", "account": {
"answer": "For print materials, we recommend SVG format for scalability or high-resolution PNG (300+ DPI) for best quality." "question": "Do I need an account to create QR codes?",
}, "answer": "No account is required for static QR codes. However, dynamic QR codes with tracking and editing capabilities require a free account."
"password": { },
"question": "Can I password-protect a QR code?", "static_vs_dynamic": {
"answer": "Yes, Pro and Business plans include password protection and access control features for your QR codes." "question": "What's the difference between static and dynamic QR codes?",
}, "answer": "Static QR codes contain fixed content that cannot be changed. Dynamic QR codes can be edited anytime and provide detailed analytics."
"analytics": { },
"question": "How do analytics work?", "forever": {
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored." "question": "Will my QR codes work forever?",
}, "answer": "Static QR codes work forever as the content is embedded directly. Dynamic QR codes work as long as your account is active."
"privacy": { },
"question": "Do you track personal data?", "file_type": {
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers." "question": "What file type should I use for printing?",
}, "answer": "For print materials, we recommend SVG format for scalability or high-resolution PNG (300+ DPI) for best quality."
"bulk": { },
"question": "Can I bulk-create codes with my own data?", "password": {
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping." "question": "Can I password-protect a QR code?",
} "answer": "Yes, Pro and Business plans include password protection and access control features for your QR codes."
} },
}, "analytics": {
"dashboard": { "question": "How do analytics work?",
"title": "Dashboard", "answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
"subtitle": "Manage your QR codes and track performance", },
"stats": { "privacy": {
"total_scans": "Total Scans", "question": "Do you track personal data?",
"active_codes": "Active QR Codes", "answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
"conversion_rate": "Conversion Rate" },
}, "bulk": {
"recent_codes": "Recent QR Codes", "question": "Can I bulk-create codes with my own data?",
"blog_resources": "Blog & Resources", "answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
"menu": { }
"edit": "Edit", }
"duplicate": "Duplicate", },
"pause": "Pause", "dashboard": {
"delete": "Delete" "title": "Dashboard",
} "subtitle": "Manage your QR codes and track performance",
}, "stats": {
"create": { "total_scans": "Total Scans",
"title": "Create QR Code", "active_codes": "Active QR Codes",
"subtitle": "Generate dynamic and static QR codes with custom branding", "conversion_rate": "Conversion Rate"
"content": "Content", },
"type": "QR Code Type", "recent_codes": "Recent QR Codes",
"style": "Style & Branding", "blog_resources": "Blog & Resources",
"preview": "Live Preview", "menu": {
"title_label": "Title", "edit": "Edit",
"title_placeholder": "My QR Code", "duplicate": "Duplicate",
"content_type": "Content Type", "pause": "Pause",
"url_label": "URL", "delete": "Delete"
"url_placeholder": "https://example.com", }
"tags_label": "Tags (comma-separated)", },
"tags_placeholder": "marketing, campaign, 2025", "create": {
"qr_code_type": "QR Code Type", "title": "Create QR Code",
"dynamic": "Dynamic", "subtitle": "Generate dynamic and static QR codes with custom branding",
"static": "Static", "content": "Content",
"recommended": "Recommended", "type": "QR Code Type",
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.", "style": "Style & Branding",
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.", "preview": "Live Preview",
"foreground_color": "Foreground Color", "title_label": "Title",
"background_color": "Background Color", "title_placeholder": "My QR Code",
"corner_style": "Corner Style", "content_type": "Content Type",
"size": "Size", "url_label": "URL",
"good_contrast": "Good contrast", "url_placeholder": "https://example.com",
"contrast_ratio": "Contrast ratio", "tags_label": "Tags (comma-separated)",
"download_svg": "Download SVG", "tags_placeholder": "marketing, campaign, 2025",
"download_png": "Download PNG", "qr_code_type": "QR Code Type",
"save_qr_code": "Save QR Code" "dynamic": "Dynamic",
}, "static": "Static",
"analytics": { "recommended": "Recommended",
"title": "Analytics", "dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
"subtitle": "Track and analyze your QR code performance", "static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
"export_report": "Export Report", "foreground_color": "Foreground Color",
"from_last_period": "from last period", "background_color": "Background Color",
"no_mobile_scans": "No mobile scans", "corner_style": "Corner Style",
"of_total": "of total", "size": "Size",
"ranges": { "good_contrast": "Good contrast",
"7d": "7 Days", "contrast_ratio": "Contrast ratio",
"30d": "30 Days", "download_svg": "Download SVG",
"90d": "90 Days" "download_png": "Download PNG",
}, "save_qr_code": "Save QR Code"
"kpis": { },
"total_scans": "Total Scans", "analytics": {
"avg_scans": "Avg Scans/QR", "title": "Analytics",
"mobile_usage": "Mobile Usage", "subtitle": "Track and analyze your QR code performance",
"top_country": "Top Country" "export_report": "Export Report",
}, "from_last_period": "from last period",
"charts": { "no_mobile_scans": "No mobile scans",
"scans_over_time": "Scans Over Time", "of_total": "of total",
"device_types": "Device Types", "ranges": {
"top_countries": "Top Countries" "7d": "7 Days",
}, "30d": "30 Days",
"table": { "90d": "90 Days"
"qr_code": "QR Code", },
"type": "Type", "kpis": {
"total_scans": "Total Scans", "total_scans": "Total Scans",
"unique_scans": "Unique Scans", "avg_scans": "Avg Scans/QR",
"conversion": "Conversion", "mobile_usage": "Mobile Usage",
"trend": "Trend", "top_country": "Top Country"
"scans": "Scans", },
"percentage": "Percentage", "charts": {
"country": "Country", "scans_over_time": "Scans Over Time",
"performance": "Performance", "device_types": "Device Types",
"created": "Created", "top_countries": "Top Countries"
"status": "Status" },
}, "table": {
"performance_title": "QR Code Performance" "qr_code": "QR Code",
}, "type": "Type",
"bulk": { "total_scans": "Total Scans",
"title": "Bulk Creation", "unique_scans": "Unique Scans",
"subtitle": "Create multiple QR codes at once from CSV or Excel files", "conversion": "Conversion",
"template_warning_title": "Please Follow the Template Format", "trend": "Trend",
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).", "scans": "Scans",
"static_only_title": "Static QR Codes Only", "percentage": "Percentage",
"static_only_text": "Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.", "country": "Country",
"download_template": "Download Template", "performance": "Performance",
"no_file_selected": "No file selected", "created": "Created",
"simple_format": "Simple Format", "status": "Status"
"just_title_url": "Just title & URL", },
"static_qr_codes": "Static QR Codes", "performance_title": "QR Code Performance"
"no_tracking": "No tracking included", },
"instant_download": "Instant Download", "bulk": {
"get_zip": "Get ZIP with all SVGs", "title": "Bulk Creation",
"max_rows": "max 1,000 rows", "subtitle": "Create multiple QR codes at once from CSV or Excel files",
"steps": { "template_warning_title": "Please Follow the Template Format",
"upload": "Upload File", "template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
"preview": "Preview & Map", "static_only_title": "Static QR Codes Only",
"download": "Download" "static_only_text": "Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.",
}, "download_template": "Download Template",
"drag_drop": "Drag & drop your file here", "no_file_selected": "No file selected",
"or_click": "or click to browse", "simple_format": "Simple Format",
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)" "just_title_url": "Just title & URL",
}, "static_qr_codes": "Static QR Codes",
"integrations": { "no_tracking": "No tracking included",
"title": "Integrations", "instant_download": "Instant Download",
"metrics": { "get_zip": "Get ZIP with all SVGs",
"total_codes": "QR Codes Total", "max_rows": "max 1,000 rows",
"active_integrations": "Active Integrations", "steps": {
"sync_status": "Sync Status", "upload": "Upload File",
"available_services": "Available Services" "preview": "Preview & Map",
}, "download": "Download"
"zapier": { },
"title": "Zapier", "drag_drop": "Drag & drop your file here",
"description": "Automate QR code creation with 5000+ apps", "or_click": "or click to browse",
"features": [ "supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
"Trigger on new QR codes", },
"Create codes from other apps", "integrations": {
"Sync scan data" "title": "Integrations",
] "metrics": {
}, "total_codes": "QR Codes Total",
"airtable": { "active_integrations": "Active Integrations",
"title": "Airtable", "sync_status": "Sync Status",
"description": "Sync QR codes with your Airtable bases", "available_services": "Available Services"
"features": [ },
"Two-way sync", "zapier": {
"Custom field mapping", "title": "Zapier",
"Real-time updates" "description": "Automate QR code creation with 5000+ apps",
] "features": [
}, "Trigger on new QR codes",
"sheets": { "Create codes from other apps",
"title": "Google Sheets", "Sync scan data"
"description": "Export data to Google Sheets automatically", ]
"features": [ },
"Automated exports", "airtable": {
"Custom templates", "title": "Airtable",
"Scheduled updates" "description": "Sync QR codes with your Airtable bases",
] "features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
}, },
"activate": "Activate & Configure" "sheets": {
}, "title": "Google Sheets",
"settings": { "description": "Export data to Google Sheets automatically",
"title": "Settings", "features": ["Automated exports", "Custom templates", "Scheduled updates"]
"subtitle": "Manage your account settings and preferences", },
"tabs": { "activate": "Activate & Configure"
"profile": "Profile", },
"billing": "Billing", "settings": {
"team": "Team & Roles", "title": "Settings",
"api": "API Keys", "subtitle": "Manage your account settings and preferences",
"workspace": "Workspace" "tabs": {
} "profile": "Profile",
}, "billing": "Billing",
"common": { "team": "Team & Roles",
"save": "Save", "api": "API Keys",
"cancel": "Cancel", "workspace": "Workspace"
"delete": "Delete", }
"edit": "Edit", },
"create": "Create", "common": {
"loading": "Loading...", "save": "Save",
"error": "An error occurred", "cancel": "Cancel",
"success": "Success!" "delete": "Delete",
}, "edit": "Edit",
"footer": { "create": "Create",
"product": "Product", "loading": "Loading...",
"features": "Features", "error": "An error occurred",
"pricing": "Pricing", "success": "Success!"
"faq": "FAQ", },
"blog": "Blog", "footer": {
"resources": "Resources", "product": "Product",
"full_pricing": "Full Pricing", "features": "Features",
"all_questions": "All Questions", "pricing": "Pricing",
"all_articles": "All Articles", "faq": "FAQ",
"learn": "Learn", "blog": "Blog",
"get_started": "Get Started", "resources": "Resources",
"legal": "Legal", "full_pricing": "Full Pricing",
"industries": "Industries", "all_questions": "All Questions",
"privacy_policy": "Privacy Policy", "all_articles": "All Articles",
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.", "learn": "Learn",
"newsletter": "Newsletter signup", "get_started": "Get Started",
"rights_reserved": "QR Master. All rights reserved." "legal": "Legal",
} "industries": "Industries",
} "privacy_policy": "Privacy Policy",
"tagline": "Create custom QR codes in seconds with advanced tracking and analytics.",
"newsletter": "Newsletter signup",
"rights_reserved": "QR Master. All rights reserved."
}
}

View File

@@ -33,6 +33,7 @@ export function middleware(req: NextRequest) {
// '/guide', // Redirected to /learn/* // '/guide', // Redirected to /learn/*
'/qr-code-erstellen', '/qr-code-erstellen',
'/dynamic-qr-code-generator', '/dynamic-qr-code-generator',
'/dynamic-barcode-generator',
'/bulk-qr-code-generator', '/bulk-qr-code-generator',
'/qr-code-tracking', '/qr-code-tracking',
'/qr-code-analytics', '/qr-code-analytics',