feat: implement pricing strategy, subscription tiers, and core infrastructure for QR code management
This commit is contained in:
@@ -121,6 +121,7 @@ enum ContentType {
|
||||
APP
|
||||
COUPON
|
||||
FEEDBACK
|
||||
BARCODE
|
||||
}
|
||||
|
||||
enum QRStatus {
|
||||
|
||||
@@ -22,8 +22,10 @@ interface BulkQRData {
|
||||
|
||||
interface GeneratedQR {
|
||||
title: string;
|
||||
content: string; // Original URL
|
||||
svg: string; // SVG markup
|
||||
content: string;
|
||||
svg: string;
|
||||
slug?: string;
|
||||
redirectUrl?: string;
|
||||
}
|
||||
|
||||
export default function BulkCreationPage() {
|
||||
@@ -35,16 +37,25 @@ export default function BulkCreationPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
|
||||
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(() => {
|
||||
const checkPlan = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/plan');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const [planRes, statsRes] = await Promise.all([
|
||||
fetch('/api/user/plan'),
|
||||
fetch('/api/user/stats'),
|
||||
]);
|
||||
if (planRes.ok) {
|
||||
const data = await planRes.json();
|
||||
setUserPlan(data.plan || 'FREE');
|
||||
}
|
||||
if (statsRes.ok) {
|
||||
const stats = await statsRes.json();
|
||||
setRemainingDynamic((stats.dynamicLimit || 0) - (stats.dynamicUsed || 0));
|
||||
}
|
||||
} catch (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 zip = new JSZip();
|
||||
|
||||
@@ -204,6 +267,18 @@ export default function BulkCreationPage() {
|
||||
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' });
|
||||
saveAs(blob, 'qr-codes-bulk.zip');
|
||||
showToast('Download started!', 'success');
|
||||
@@ -274,8 +349,8 @@ export default function BulkCreationPage() {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Show upgrade prompt if not Business plan
|
||||
if (userPlan !== 'BUSINESS') {
|
||||
// Show upgrade prompt if not Business or Enterprise plan
|
||||
if (userPlan !== 'BUSINESS' && userPlan !== 'ENTERPRISE') {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="mt-12">
|
||||
@@ -309,6 +384,39 @@ export default function BulkCreationPage() {
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
|
||||
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Template Warning Banner */}
|
||||
@@ -641,8 +749,13 @@ export default function BulkCreationPage() {
|
||||
<Button variant="outline" onClick={() => setStep('upload')}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={generateStaticQRCodes} loading={loading}>
|
||||
Generate {data.length} Static QR Codes
|
||||
<Button
|
||||
onClick={isDynamic ? generateDynamicQRCodes : generateStaticQRCodes}
|
||||
loading={loading}
|
||||
>
|
||||
{isDynamic
|
||||
? `Generate ${Math.min(data.length, remainingDynamic)} Dynamic QR Codes`
|
||||
: `Generate ${data.length} Static QR Codes`}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -15,8 +15,9 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
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';
|
||||
import Barcode from 'react-barcode';
|
||||
|
||||
// Tooltip component for form field help
|
||||
const Tooltip = ({ text }: { text: string }) => (
|
||||
@@ -140,6 +141,7 @@ export default function CreatePage() {
|
||||
{ value: 'APP', label: 'App Download', icon: Smartphone },
|
||||
{ value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
|
||||
{ value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
|
||||
{ value: 'BARCODE', label: 'Barcode', icon: BarcodeIcon },
|
||||
];
|
||||
|
||||
// Get QR content based on content type
|
||||
@@ -170,6 +172,8 @@ export default function CreatePage() {
|
||||
return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
|
||||
case 'FEEDBACK':
|
||||
return content.feedbackUrl || 'https://example.com/feedback';
|
||||
case 'BARCODE':
|
||||
return content.value || '123456789';
|
||||
default:
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
@@ -992,7 +1058,25 @@ export default function CreatePage() {
|
||||
</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' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrContent}
|
||||
|
||||
@@ -165,6 +165,8 @@ export default function SettingsPage() {
|
||||
return { dynamic: 50, price: '€9', period: 'per month' };
|
||||
case 'BUSINESS':
|
||||
return { dynamic: 500, price: '€29', period: 'per month' };
|
||||
case 'ENTERPRISE':
|
||||
return { dynamic: 99999, price: 'Custom', period: 'per month' };
|
||||
default:
|
||||
return { dynamic: 3, price: '€0', period: 'forever' };
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import { Button } from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
|
||||
type LoginClientProps = {
|
||||
showPageHeading?: boolean;
|
||||
};
|
||||
|
||||
export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
|
||||
type LoginClientProps = {
|
||||
showPageHeading?: boolean;
|
||||
};
|
||||
|
||||
export default function LoginClient({ showPageHeading = true }: LoginClientProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation();
|
||||
@@ -22,6 +22,7 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
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="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
|
||||
<img src="/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>
|
||||
</Link>
|
||||
{showPageHeading ? (
|
||||
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
|
||||
) : (
|
||||
<h2 className="text-3xl font-bold text-gray-900">Welcome Back</h2>
|
||||
)}
|
||||
<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" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
{showPageHeading ? (
|
||||
<h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
|
||||
) : (
|
||||
<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>
|
||||
<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
|
||||
@@ -112,14 +113,37 @@ export default function LoginClient({ showPageHeading = true }: LoginClientProps
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
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">
|
||||
<label className="flex items-center">
|
||||
|
||||
@@ -19,6 +19,8 @@ export default function SignupClient() {
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
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="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-flex items-center space-x-2 mb-6">
|
||||
<img src="/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>
|
||||
</Link>
|
||||
<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" />
|
||||
<span className="text-2xl font-bold text-gray-900">QR Master</span>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
|
||||
<p className="text-gray-600 mt-2">Start creating QR codes in seconds</p>
|
||||
<Link href="/" className="text-sm text-primary-600 hover:text-primary-700 font-medium mt-2 inline-block border border-primary-600 hover:border-primary-700 px-4 py-2 rounded-lg transition-colors">
|
||||
@@ -126,23 +128,69 @@ export default function SignupClient() {
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
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
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">Confirm Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="confirm-password"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
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}>
|
||||
Create Account
|
||||
|
||||
@@ -1,269 +1,315 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
||||
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null);
|
||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch current user plan
|
||||
const fetchUserPlan = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/plan');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentPlan(data.plan || 'FREE');
|
||||
setCurrentInterval(data.interval || null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserPlan();
|
||||
}, []);
|
||||
|
||||
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
|
||||
setLoading(plan);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/create-checkout-session', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plan,
|
||||
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create checkout session');
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
showToast('Failed to start checkout. Please try again.', 'error');
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDowngrade = async () => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading('FREE');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/cancel-subscription', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to cancel subscription');
|
||||
}
|
||||
|
||||
showToast('Successfully downgraded to Free plan', 'success');
|
||||
|
||||
// Refresh to update the plan
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
console.error('Error canceling subscription:', error);
|
||||
showToast(error.message || 'Failed to downgrade. Please try again.', 'error');
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to check if this is the user's exact current plan (plan + interval)
|
||||
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => {
|
||||
return currentPlan === planType && currentInterval === interval;
|
||||
};
|
||||
|
||||
// Helper function to check if user has this plan but different interval
|
||||
const hasPlanDifferentInterval = (planType: string) => {
|
||||
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod;
|
||||
};
|
||||
|
||||
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 'free',
|
||||
name: 'Free',
|
||||
price: '€0',
|
||||
period: 'forever',
|
||||
showDiscount: false,
|
||||
features: [
|
||||
'3 active dynamic QR codes (8 types available)',
|
||||
'Unlimited static QR codes',
|
||||
'Basic scan tracking',
|
||||
'Standard QR design templates',
|
||||
'Download as SVG/PNG',
|
||||
],
|
||||
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
|
||||
buttonVariant: 'outline' as const,
|
||||
disabled: currentPlan === 'FREE',
|
||||
popular: false,
|
||||
onDowngrade: handleDowngrade,
|
||||
},
|
||||
{
|
||||
key: 'pro',
|
||||
name: 'Pro',
|
||||
price: billingPeriod === 'month' ? '€9' : '€90',
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'50 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Advanced analytics (scans, devices, locations)',
|
||||
'Custom branding (colors & logos)',
|
||||
],
|
||||
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('PRO')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Pro',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
|
||||
popular: true,
|
||||
onUpgrade: () => handleUpgrade('PRO'),
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
name: 'Business',
|
||||
price: billingPeriod === 'month' ? '€29' : '€290',
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'500 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Everything from Pro',
|
||||
'Bulk QR Creation (up to 1,000)',
|
||||
'Priority email support',
|
||||
'Advanced tracking & insights',
|
||||
],
|
||||
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('BUSINESS')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Business',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
|
||||
popular: false,
|
||||
onUpgrade: () => handleUpgrade('BUSINESS'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Choose Your Plan
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Select the perfect plan for your QR code needs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.key}
|
||||
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<Badge variant="info" className="px-3 py-1">
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-8">
|
||||
<CardTitle className="text-2xl mb-4">
|
||||
{plan.name}
|
||||
</CardTitle>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{plan.price}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.period}
|
||||
</span>
|
||||
</div>
|
||||
{plan.showDiscount && (
|
||||
<Badge variant="success" className="mt-2">
|
||||
Save 16%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<ul className="space-y-3">
|
||||
{plan.features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
variant={plan.buttonVariant}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={plan.disabled || loading === plan.key.toUpperCase()}
|
||||
onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade}
|
||||
>
|
||||
{loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<p className="text-gray-600">
|
||||
All plans include unlimited static QR codes and basic customization.
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Need help choosing? <ObfuscatedMailto email="support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</ObfuscatedMailto>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
import { ObfuscatedMailto } from '@/components/ui/ObfuscatedMailto';
|
||||
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
|
||||
const [currentInterval, setCurrentInterval] = useState<
|
||||
'month' | 'year' | null
|
||||
>(null);
|
||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch current user plan
|
||||
const fetchUserPlan = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/user/plan');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentPlan(data.plan || 'FREE');
|
||||
setCurrentInterval(data.interval || null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user plan:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserPlan();
|
||||
}, []);
|
||||
|
||||
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
|
||||
setLoading(plan);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/create-checkout-session', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plan,
|
||||
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create checkout session');
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error('Error creating checkout session:', error);
|
||||
showToast('Failed to start checkout. Please try again.', 'error');
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDowngrade = async () => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading('FREE');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/cancel-subscription', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to cancel subscription');
|
||||
}
|
||||
|
||||
showToast('Successfully downgraded to Free plan', 'success');
|
||||
|
||||
// Refresh to update the plan
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
console.error('Error canceling subscription:', error);
|
||||
showToast(
|
||||
error.message || 'Failed to downgrade. Please try again.',
|
||||
'error'
|
||||
);
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to check if this is the user's exact current plan (plan + interval)
|
||||
const isCurrentPlanWithInterval = (
|
||||
planType: string,
|
||||
interval: 'month' | 'year'
|
||||
) => {
|
||||
return currentPlan === planType && currentInterval === interval;
|
||||
};
|
||||
|
||||
// Helper function to check if user has this plan but different interval
|
||||
const hasPlanDifferentInterval = (planType: string) => {
|
||||
return (
|
||||
currentPlan === planType &&
|
||||
currentInterval &&
|
||||
currentInterval !== billingPeriod
|
||||
);
|
||||
};
|
||||
|
||||
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 'free',
|
||||
name: 'Free',
|
||||
price: '€0',
|
||||
period: 'forever',
|
||||
showDiscount: false,
|
||||
features: [
|
||||
'3 active dynamic QR codes (8 types available)',
|
||||
'Unlimited static QR codes',
|
||||
'Basic scan tracking',
|
||||
'Standard QR design templates',
|
||||
'Download as SVG/PNG',
|
||||
],
|
||||
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
|
||||
buttonVariant: 'outline' as const,
|
||||
disabled: currentPlan === 'FREE',
|
||||
popular: false,
|
||||
onDowngrade: handleDowngrade,
|
||||
},
|
||||
{
|
||||
key: 'pro',
|
||||
name: 'Pro',
|
||||
price: billingPeriod === 'month' ? '€9' : '€90',
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'50 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Advanced analytics (scans, devices, locations)',
|
||||
'Custom branding (colors & logos)',
|
||||
],
|
||||
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('PRO')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Pro',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
|
||||
popular: true,
|
||||
onUpgrade: () => handleUpgrade('PRO'),
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
name: 'Business',
|
||||
price: billingPeriod === 'month' ? '€29' : '€290',
|
||||
period: billingPeriod === 'month' ? 'per month' : 'per year',
|
||||
showDiscount: billingPeriod === 'year',
|
||||
features: [
|
||||
'500 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Everything from Pro',
|
||||
'Bulk QR Creation (up to 1,000)',
|
||||
'Priority email support',
|
||||
'Advanced tracking & insights',
|
||||
],
|
||||
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('BUSINESS')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Business',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
|
||||
popular: false,
|
||||
onUpgrade: () => handleUpgrade('BUSINESS'),
|
||||
},
|
||||
{
|
||||
key: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
price: 'Custom',
|
||||
period: '',
|
||||
showDiscount: false,
|
||||
features: [
|
||||
'∞ dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Everything from Business',
|
||||
'Dedicated Account Manager',
|
||||
],
|
||||
buttonText: 'Contact Us',
|
||||
buttonVariant: 'outline' as const,
|
||||
disabled: false,
|
||||
popular: false,
|
||||
onUpgrade: () => (window.location.href = 'mailto:timo@qrmaster.net'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Choose Your Plan
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Select the perfect plan for your QR code needs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.key}
|
||||
className={
|
||||
plan.popular ? 'border-primary-500 shadow-xl relative' : ''
|
||||
}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<Badge variant="info" className="px-3 py-1">
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-8">
|
||||
<CardTitle className="text-2xl mb-4">{plan.name}</CardTitle>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">{plan.price}</span>
|
||||
<span className="text-gray-600 ml-2">{plan.period}</span>
|
||||
</div>
|
||||
{plan.showDiscount && (
|
||||
<Badge variant="success" className="mt-2">
|
||||
Save 16%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<ul className="space-y-3">
|
||||
{plan.features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
variant={plan.buttonVariant}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={plan.disabled || loading === plan.key.toUpperCase()}
|
||||
onClick={
|
||||
plan.key === 'free'
|
||||
? (plan as any).onDowngrade
|
||||
: (plan as any).onUpgrade
|
||||
}
|
||||
>
|
||||
{loading === plan.key.toUpperCase()
|
||||
? 'Processing...'
|
||||
: plan.buttonText}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<p className="text-gray-600">
|
||||
All plans include unlimited static QR codes and basic customization.
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Need help choosing?{' '}
|
||||
<ObfuscatedMailto
|
||||
email="support@qrmaster.net"
|
||||
className="text-primary-600 hover:text-primary-700 underline"
|
||||
>
|
||||
Contact our team
|
||||
</ObfuscatedMailto>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const PLAN_LIMITS = {
|
||||
FREE: 3,
|
||||
PRO: 50,
|
||||
BUSINESS: 500,
|
||||
ENTERPRISE: 99999,
|
||||
};
|
||||
|
||||
// POST /api/qrs - Create a new QR code
|
||||
|
||||
@@ -1,62 +1,64 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with plan info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Count dynamic QR codes
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'DYNAMIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Count static QR codes
|
||||
const staticQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'STATIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Determine limits based on plan
|
||||
let dynamicLimit = 3; // FREE plan default
|
||||
if (user.plan === 'PRO') {
|
||||
dynamicLimit = 50;
|
||||
} else if (user.plan === 'BUSINESS') {
|
||||
dynamicLimit = 500;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
dynamicUsed: dynamicQRCount,
|
||||
dynamicLimit,
|
||||
staticUsed: staticQRCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user stats:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userId = cookies().get('userId')?.value;
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user with plan info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Count dynamic QR codes
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'DYNAMIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Count static QR codes
|
||||
const staticQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'STATIC',
|
||||
},
|
||||
});
|
||||
|
||||
// Determine limits based on plan
|
||||
let dynamicLimit = 3; // FREE plan default
|
||||
if (user.plan === 'PRO') {
|
||||
dynamicLimit = 50;
|
||||
} else if (user.plan === 'BUSINESS') {
|
||||
dynamicLimit = 500;
|
||||
} else if ((user.plan as string) === 'ENTERPRISE') {
|
||||
dynamicLimit = 99999;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
dynamicUsed: dynamicQRCount,
|
||||
dynamicLimit,
|
||||
staticUsed: staticQRCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching user stats:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ export async function GET(
|
||||
const baseUrlFeedback = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3050';
|
||||
destination = `${baseUrlFeedback}/feedback/${slug}`;
|
||||
break;
|
||||
case 'BARCODE':
|
||||
// Dynamic barcode redirects to its stored URL
|
||||
destination = ensureAbsoluteUrl(content.url || content.value || 'https://example.com');
|
||||
break;
|
||||
default:
|
||||
destination = 'https://example.com';
|
||||
}
|
||||
|
||||
@@ -206,6 +206,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/dynamic-barcode-generator`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/bulk-qr-code-generator`,
|
||||
lastModified: new Date(),
|
||||
|
||||
@@ -1,141 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
|
||||
interface PricingProps {
|
||||
t: any; // i18n translation function
|
||||
}
|
||||
|
||||
export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 'free',
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
key: 'pro',
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
popular: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<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 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t.pricing.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
{t.pricing.subtitle}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{plans.map((plan, index) => (
|
||||
<motion.div
|
||||
key={plan.key}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Card
|
||||
className={`h-full flex flex-col ${plan.popular
|
||||
? 'border-primary-500 shadow-xl relative scale-105 z-10'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<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}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-8">
|
||||
<CardTitle className="text-2xl mb-4">
|
||||
{t.pricing[plan.key].title}
|
||||
</CardTitle>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{plan.key === 'free'
|
||||
? t.pricing[plan.key].price
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].price
|
||||
: plan.key === 'pro'
|
||||
? '€90'
|
||||
: '€290'}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.key === 'free'
|
||||
? t.pricing[plan.key].period
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].period
|
||||
: 'per year'}
|
||||
</span>
|
||||
</div>
|
||||
{billingPeriod === 'year' && plan.key !== 'free' && (
|
||||
<Badge variant="success" className="mt-2">
|
||||
Save 16%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-8 flex-1 flex flex-col">
|
||||
<ul className="space-y-3 flex-1">
|
||||
{t.pricing[plan.key].features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-100">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { BillingToggle } from '@/components/ui/BillingToggle';
|
||||
|
||||
interface PricingProps {
|
||||
t: any; // i18n translation function
|
||||
}
|
||||
|
||||
export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
||||
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
|
||||
|
||||
const plans = [
|
||||
{
|
||||
key: 'free',
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
key: 'pro',
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
key: 'enterprise',
|
||||
popular: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<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 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
|
||||
{t.pricing.title}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">{t.pricing.subtitle}</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
|
||||
{plans.map((plan, index) => (
|
||||
<motion.div
|
||||
key={plan.key}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Card
|
||||
className={`h-full flex flex-col ${
|
||||
plan.popular
|
||||
? 'border-primary-500 shadow-xl relative scale-105 z-10'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:shadow-lg transition-all'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<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}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-8">
|
||||
<CardTitle className="text-2xl mb-4">
|
||||
{t.pricing[plan.key].title}
|
||||
</CardTitle>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-baseline justify-center">
|
||||
<span className="text-4xl font-bold">
|
||||
{plan.key === 'free' || plan.key === 'enterprise'
|
||||
? t.pricing[plan.key].price
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].price
|
||||
: plan.key === 'pro'
|
||||
? '€90'
|
||||
: '€290'}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.key === 'free' || plan.key === 'enterprise'
|
||||
? t.pricing[plan.key].period
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].period
|
||||
: 'per year'}
|
||||
</span>
|
||||
</div>
|
||||
{billingPeriod === 'year' &&
|
||||
plan.key !== 'free' &&
|
||||
plan.key !== 'enterprise' && (
|
||||
<Badge variant="success" className="mt-2">
|
||||
Save 16%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-8 flex-1 flex flex-col">
|
||||
<ul className="space-y-3 flex-1">
|
||||
{t.pricing[plan.key].features.map(
|
||||
(feature: string, index: number) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-100">
|
||||
{plan.key === 'enterprise' ? (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
|
||||
<div>
|
||||
<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" />
|
||||
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
|
||||
</Link>
|
||||
<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" />
|
||||
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
|
||||
</Link>
|
||||
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
|
||||
{translations.tagline}
|
||||
</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="/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="/dynamic-barcode-generator" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Dynamic Barcode Generator</Link></li>
|
||||
|
||||
<li>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3>
|
||||
<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="/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/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>
|
||||
<div>
|
||||
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>Compare</h3>
|
||||
<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="/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/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/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>
|
||||
</ul>
|
||||
@@ -136,7 +137,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<p>© 2026 {translations.rights_reserved}</p>
|
||||
<p>© 2026 {translations.rights_reserved}</p>
|
||||
<div className="w-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
811
src/i18n/de.json
811
src/i18n/de.json
@@ -1,408 +1,421 @@
|
||||
{
|
||||
"nav": {
|
||||
"features": "Funktionen",
|
||||
"pricing": "Preise",
|
||||
"faq": "FAQ",
|
||||
"blog": "Blog",
|
||||
"login": "Anmelden",
|
||||
"dashboard": "Dashboard",
|
||||
"about": "Über uns",
|
||||
"contact": "Kontakt",
|
||||
"signup": "Registrieren",
|
||||
"learn": "Lernen",
|
||||
"create_qr": "QR erstellen",
|
||||
"bulk_creation": "Massen-Erstellung",
|
||||
"analytics": "Analytik",
|
||||
"settings": "Einstellungen",
|
||||
"cta": "Kostenlos starten",
|
||||
{
|
||||
"nav": {
|
||||
"features": "Funktionen",
|
||||
"pricing": "Preise",
|
||||
"faq": "FAQ",
|
||||
"blog": "Blog",
|
||||
"login": "Anmelden",
|
||||
"dashboard": "Dashboard",
|
||||
"about": "Über uns",
|
||||
"contact": "Kontakt",
|
||||
"signup": "Registrieren",
|
||||
"learn": "Lernen",
|
||||
"create_qr": "QR erstellen",
|
||||
"bulk_creation": "Massen-Erstellung",
|
||||
"analytics": "Analytik",
|
||||
"settings": "Einstellungen",
|
||||
"cta": "Kostenlos starten",
|
||||
"tools": "Kostenlose Tools",
|
||||
"all_free": "Alle Generatoren sind 100% kostenlos",
|
||||
"resources": "Ressourcen",
|
||||
"all_industries": "Branchen"
|
||||
},
|
||||
"hero": {
|
||||
"badge": "Kostenloser QR-Code-Generator",
|
||||
"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.",
|
||||
"features": [
|
||||
"Keine Kreditkarte zum Starten erforderlich",
|
||||
"QR-Codes für immer kostenlos erstellen",
|
||||
"Erweiterte Verfolgung und Analytik",
|
||||
"Individuelle Farben und Stile"
|
||||
],
|
||||
"cta_primary": "QR-Code kostenlos erstellen",
|
||||
"cta_secondary": "Preise ansehen",
|
||||
"engagement_badge": "Kostenlos für immer",
|
||||
"get_started": "Loslegen",
|
||||
"view_full_pricing": "Alle Preisdetails ansehen →"
|
||||
},
|
||||
"trust": {
|
||||
"users": "Aktive Nutzer",
|
||||
"codes": "QR-Codes erstellt",
|
||||
"scans": "Scans verfolgt",
|
||||
"countries": "Länder"
|
||||
},
|
||||
"industries": {
|
||||
"restaurant": "Restaurant-Kette",
|
||||
"tech": "Tech-Startup",
|
||||
"realestate": "Immobilien",
|
||||
"events": "Event-Agentur",
|
||||
"retail": "Einzelhandel",
|
||||
"healthcare": "Gesundheitswesen"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Mit einer Vorlage beginnen",
|
||||
"restaurant": "Restaurant-Menü",
|
||||
"business": "Visitenkarte",
|
||||
"vcard": "Kontaktkarte",
|
||||
"event": "Event-Ticket",
|
||||
"use_template": "Vorlage verwenden →"
|
||||
},
|
||||
"generator": {
|
||||
"title": "Sofortiger QR-Code-Generator",
|
||||
"url_placeholder": "Geben Sie hier Ihre URL ein...",
|
||||
"foreground": "Vordergrund",
|
||||
"background": "Hintergrund",
|
||||
"corners": "Ecken",
|
||||
"size": "Größe",
|
||||
"contrast_good": "Guter Kontrast",
|
||||
"download_svg": "SVG herunterladen",
|
||||
"download_png": "PNG herunterladen",
|
||||
"save_track": "Speichern & Verfolgen",
|
||||
"live_preview": "Live-Vorschau",
|
||||
"demo_note": "Dies ist ein Demo-QR-Code"
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"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.",
|
||||
"static": {
|
||||
"title": "Statische QR-Codes",
|
||||
"subtitle": "Immer kostenlos",
|
||||
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
|
||||
"features": [
|
||||
"Inhalt kann nicht bearbeitet werden",
|
||||
"Keine Scan-Verfolgung",
|
||||
"Funktioniert für immer",
|
||||
"Kein Konto erforderlich"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"title": "Dynamische QR-Codes",
|
||||
"subtitle": "Empfohlen",
|
||||
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
|
||||
"features": [
|
||||
"Inhalt jederzeit bearbeiten",
|
||||
"Erweiterte Analytik",
|
||||
"Individuelles Branding",
|
||||
"Bulk-Operationen"
|
||||
]
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
|
||||
"analytics": {
|
||||
"title": "Erweiterte Analytik",
|
||||
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Vollständige Anpassung",
|
||||
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
|
||||
},
|
||||
"unlimited": {
|
||||
"title": "Unbegrenzte statische QR-Codes",
|
||||
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk-Operationen",
|
||||
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrationen",
|
||||
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
|
||||
},
|
||||
"api": {
|
||||
"title": "Entwickler-API",
|
||||
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
|
||||
},
|
||||
"support": {
|
||||
"title": "24/7 Support",
|
||||
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Wählen Sie Ihren Plan",
|
||||
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
||||
"choose_plan": "Wählen Sie Ihren Plan",
|
||||
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
||||
"current_plan": "Aktueller Plan",
|
||||
"upgrade_to": "Upgrade auf",
|
||||
"downgrade_to_free": "Zu Kostenlos zurückstufen",
|
||||
"most_popular": "Beliebteste",
|
||||
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
|
||||
"free": {
|
||||
"title": "Kostenlos",
|
||||
"name": "Free",
|
||||
"price": "€0",
|
||||
"period": "für immer",
|
||||
"features": [
|
||||
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Basis-Scan-Tracking",
|
||||
"Standard QR-Design-Vorlagen"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
"title": "Pro",
|
||||
"name": "Pro",
|
||||
"price": "€9",
|
||||
"period": "pro Monat",
|
||||
"badge": "Beliebteste",
|
||||
"features": [
|
||||
"50 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Erweiterte Analytik (Scans, Geräte, Standorte)",
|
||||
"Individuelles Branding (Farben)",
|
||||
"Download als SVG/PNG"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
"title": "Business",
|
||||
"name": "Business",
|
||||
"price": "€29",
|
||||
"period": "pro Monat",
|
||||
"features": [
|
||||
"500 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Alles aus Pro",
|
||||
"Massen-QR-Erstellung (bis zu 1.000)",
|
||||
"Prioritäts-E-Mail-Support",
|
||||
"Erweiterte Tracking & Insights"
|
||||
]
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"title": "Häufig gestellte Fragen",
|
||||
"questions": {
|
||||
"account": {
|
||||
"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."
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"file_type": {
|
||||
"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."
|
||||
},
|
||||
"password": {
|
||||
"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": {
|
||||
"question": "Wie funktioniert die Analytik?",
|
||||
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
|
||||
},
|
||||
"privacy": {
|
||||
"question": "Verfolgen Sie persönliche Daten?",
|
||||
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
|
||||
},
|
||||
"bulk": {
|
||||
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
|
||||
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
|
||||
"stats": {
|
||||
"total_scans": "Gesamte Scans",
|
||||
"active_codes": "Aktive QR-Codes",
|
||||
"conversion_rate": "Konversionsrate"
|
||||
},
|
||||
"recent_codes": "Aktuelle QR-Codes",
|
||||
"blog_resources": "Blog & Ressourcen",
|
||||
"menu": {
|
||||
"edit": "Bearbeiten",
|
||||
"duplicate": "Duplizieren",
|
||||
"pause": "Pausieren",
|
||||
"delete": "Löschen"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "QR-Code erstellen",
|
||||
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
|
||||
"content": "Inhalt",
|
||||
"type": "QR-Code-Typ",
|
||||
"style": "Stil & Branding",
|
||||
"preview": "Live-Vorschau",
|
||||
"title_label": "Titel",
|
||||
"title_placeholder": "Mein QR-Code",
|
||||
"content_type": "Inhaltstyp",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://beispiel.de",
|
||||
"tags_label": "Tags (durch Komma getrennt)",
|
||||
"tags_placeholder": "marketing, kampagne, 2025",
|
||||
"qr_code_type": "QR-Code-Typ",
|
||||
"dynamic": "Dynamisch",
|
||||
"static": "Statisch",
|
||||
"recommended": "Empfohlen",
|
||||
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
|
||||
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
|
||||
"foreground_color": "Vordergrundfarbe",
|
||||
"background_color": "Hintergrundfarbe",
|
||||
"corner_style": "Eckenstil",
|
||||
"size": "Größe",
|
||||
"good_contrast": "Guter Kontrast",
|
||||
"contrast_ratio": "Kontrastverhältnis",
|
||||
"download_svg": "SVG herunterladen",
|
||||
"download_png": "PNG herunterladen",
|
||||
"save_qr_code": "QR-Code speichern"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytik",
|
||||
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
|
||||
"export_report": "Bericht exportieren",
|
||||
"from_last_period": "vom letzten Zeitraum",
|
||||
"no_mobile_scans": "Keine mobilen Scans",
|
||||
"of_total": "der Gesamtmenge",
|
||||
"ranges": {
|
||||
"7d": "7 Tage",
|
||||
"30d": "30 Tage",
|
||||
"90d": "90 Tage"
|
||||
},
|
||||
"kpis": {
|
||||
"total_scans": "Gesamte Scans",
|
||||
"avg_scans": "Ø Scans/QR",
|
||||
"mobile_usage": "Mobile Nutzung",
|
||||
"top_country": "Top Land"
|
||||
},
|
||||
"charts": {
|
||||
"scans_over_time": "Scans über Zeit",
|
||||
"device_types": "Gerätetypen",
|
||||
"top_countries": "Top Länder"
|
||||
},
|
||||
"table": {
|
||||
"qr_code": "QR-Code",
|
||||
"type": "Typ",
|
||||
"total_scans": "Gesamte Scans",
|
||||
"unique_scans": "Einzigartige Scans",
|
||||
"conversion": "Konversion",
|
||||
"trend": "Trend",
|
||||
"scans": "Scans",
|
||||
"percentage": "Prozent",
|
||||
"country": "Land",
|
||||
"performance": "Performance",
|
||||
"created": "Erstellt",
|
||||
"status": "Status"
|
||||
},
|
||||
"performance_title": "QR-Code-Performance"
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Massen-Erstellung",
|
||||
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
|
||||
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
|
||||
"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.",
|
||||
"static_only_title": "Nur statische QR-Codes",
|
||||
"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",
|
||||
"no_file_selected": "Keine ausgewählt",
|
||||
"simple_format": "Einfaches Format",
|
||||
"just_title_url": "Nur Titel & URL",
|
||||
"static_qr_codes": "Statische QR-Codes",
|
||||
"no_tracking": "Kein Tracking enthalten",
|
||||
"instant_download": "Sofortiger Download",
|
||||
"get_zip": "ZIP mit allen SVGs erhalten",
|
||||
"max_rows": "max 1.000 Zeilen",
|
||||
"steps": {
|
||||
"upload": "Datei hochladen",
|
||||
"preview": "Vorschau & Zuordnung",
|
||||
"download": "Herunterladen"
|
||||
},
|
||||
"drag_drop": "Datei hier hinziehen",
|
||||
"or_click": "oder klicken zum Durchsuchen",
|
||||
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrationen",
|
||||
"metrics": {
|
||||
"total_codes": "QR-Codes Gesamt",
|
||||
"active_integrations": "Aktive Integrationen",
|
||||
"sync_status": "Sync-Status",
|
||||
"available_services": "Verfügbare Services"
|
||||
},
|
||||
"zapier": {
|
||||
"title": "Zapier",
|
||||
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
|
||||
"features": [
|
||||
"Trigger bei neuen QR-Codes",
|
||||
"Codes aus anderen Apps erstellen",
|
||||
"Scan-Daten synchronisieren"
|
||||
]
|
||||
},
|
||||
"airtable": {
|
||||
"title": "Airtable",
|
||||
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
|
||||
"features": [
|
||||
"Bidirektionale Synchronisation",
|
||||
"Individuelle Feldzuordnung",
|
||||
"Echtzeit-Updates"
|
||||
]
|
||||
},
|
||||
"sheets": {
|
||||
"title": "Google Sheets",
|
||||
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
|
||||
"features": [
|
||||
"Automatisierte Exporte",
|
||||
"Individuelle Vorlagen",
|
||||
"Geplante Updates"
|
||||
]
|
||||
},
|
||||
"activate": "Aktivieren & Konfigurieren"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
|
||||
"tabs": {
|
||||
"profile": "Profil",
|
||||
"billing": "Abrechnung",
|
||||
"team": "Team & Rollen",
|
||||
"api": "API-Schlüssel",
|
||||
"workspace": "Arbeitsbereich"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"create": "Erstellen",
|
||||
"loading": "Lädt...",
|
||||
"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",
|
||||
"hero": {
|
||||
"badge": "Kostenloser QR-Code-Generator",
|
||||
"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.",
|
||||
"features": [
|
||||
"Keine Kreditkarte zum Starten erforderlich",
|
||||
"QR-Codes für immer kostenlos erstellen",
|
||||
"Erweiterte Verfolgung und Analytik",
|
||||
"Individuelle Farben und Stile"
|
||||
],
|
||||
"cta_primary": "QR-Code kostenlos erstellen",
|
||||
"cta_secondary": "Preise ansehen",
|
||||
"engagement_badge": "Kostenlos für immer",
|
||||
"get_started": "Loslegen",
|
||||
"view_full_pricing": "Alle Preisdetails ansehen →"
|
||||
},
|
||||
"trust": {
|
||||
"users": "Aktive Nutzer",
|
||||
"codes": "QR-Codes erstellt",
|
||||
"scans": "Scans verfolgt",
|
||||
"countries": "Länder"
|
||||
},
|
||||
"industries": {
|
||||
"restaurant": "Restaurant-Kette",
|
||||
"tech": "Tech-Startup",
|
||||
"realestate": "Immobilien",
|
||||
"events": "Event-Agentur",
|
||||
"retail": "Einzelhandel",
|
||||
"healthcare": "Gesundheitswesen"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Mit einer Vorlage beginnen",
|
||||
"restaurant": "Restaurant-Menü",
|
||||
"business": "Visitenkarte",
|
||||
"vcard": "Kontaktkarte",
|
||||
"event": "Event-Ticket",
|
||||
"use_template": "Vorlage verwenden →"
|
||||
},
|
||||
"generator": {
|
||||
"title": "Sofortiger QR-Code-Generator",
|
||||
"url_placeholder": "Geben Sie hier Ihre URL ein...",
|
||||
"foreground": "Vordergrund",
|
||||
"background": "Hintergrund",
|
||||
"corners": "Ecken",
|
||||
"size": "Größe",
|
||||
"contrast_good": "Guter Kontrast",
|
||||
"download_svg": "SVG herunterladen",
|
||||
"download_png": "PNG herunterladen",
|
||||
"save_track": "Speichern & Verfolgen",
|
||||
"live_preview": "Live-Vorschau",
|
||||
"demo_note": "Dies ist ein Demo-QR-Code"
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"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.",
|
||||
"static": {
|
||||
"title": "Statische QR-Codes",
|
||||
"subtitle": "Immer kostenlos",
|
||||
"description": "Perfekt für permanente Inhalte, die sich nie ändern",
|
||||
"features": [
|
||||
"Inhalt kann nicht bearbeitet werden",
|
||||
"Keine Scan-Verfolgung",
|
||||
"Funktioniert für immer",
|
||||
"Kein Konto erforderlich"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"title": "Dynamische QR-Codes",
|
||||
"subtitle": "Empfohlen",
|
||||
"description": "Volle Kontrolle mit Tracking- und Bearbeitungsfunktionen",
|
||||
"features": [
|
||||
"Inhalt jederzeit bearbeiten",
|
||||
"Erweiterte Analytik",
|
||||
"Individuelles Branding",
|
||||
"Bulk-Operationen"
|
||||
]
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Alles was Sie brauchen, um professionelle QR-Codes zu erstellen",
|
||||
"analytics": {
|
||||
"title": "Erweiterte Analytik",
|
||||
"description": "Verfolgen Sie Scans, Standorte, Geräte und Nutzerverhalten mit detaillierten Einblicken."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Vollständige Anpassung",
|
||||
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
|
||||
},
|
||||
"unlimited": {
|
||||
"title": "Unbegrenzte statische QR-Codes",
|
||||
"description": "Erstellen Sie so viele statische QR-Codes wie Sie benötigen. Kostenlos für immer, ohne Limits."
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk-Operationen",
|
||||
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrationen",
|
||||
"description": "Verbinden Sie sich mit Zapier, Airtable, Google Sheets und weiteren beliebten Tools."
|
||||
},
|
||||
"api": {
|
||||
"title": "Entwickler-API",
|
||||
"description": "Integrieren Sie QR-Code-Generierung in Ihre Anwendungen mit unserer REST-API."
|
||||
},
|
||||
"support": {
|
||||
"title": "24/7 Support",
|
||||
"description": "Erhalten Sie Hilfe, wenn Sie sie brauchen, mit unserem dedizierten Kundensupport-Team."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Wählen Sie Ihren Plan",
|
||||
"subtitle": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
||||
"choose_plan": "Wählen Sie Ihren Plan",
|
||||
"select_plan": "Wählen Sie den perfekten Plan für Ihre QR-Code-Bedürfnisse",
|
||||
"current_plan": "Aktueller Plan",
|
||||
"upgrade_to": "Upgrade auf",
|
||||
"downgrade_to_free": "Zu Kostenlos zurückstufen",
|
||||
"most_popular": "Beliebteste",
|
||||
"all_plans_note": "Alle Pläne beinhalten unbegrenzte statische QR-Codes und Basis-Anpassung.",
|
||||
"free": {
|
||||
"title": "Kostenlos",
|
||||
"name": "Free",
|
||||
"price": "€0",
|
||||
"period": "für immer",
|
||||
"features": [
|
||||
"3 aktive dynamische QR-Codes (8 Typen verfügbar)",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Basis-Scan-Tracking",
|
||||
"Standard QR-Design-Vorlagen"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
"title": "Pro",
|
||||
"name": "Pro",
|
||||
"price": "€9",
|
||||
"period": "pro Monat",
|
||||
"badge": "Beliebteste",
|
||||
"features": [
|
||||
"50 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Erweiterte Analytik (Scans, Geräte, Standorte)",
|
||||
"Individuelles Branding (Farben)",
|
||||
"Download als SVG/PNG"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
"title": "Business",
|
||||
"name": "Business",
|
||||
"price": "€29",
|
||||
"period": "pro Monat",
|
||||
"features": [
|
||||
"500 dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Alles aus Pro",
|
||||
"Massen-QR-Erstellung (bis zu 1.000)",
|
||||
"Prioritäts-E-Mail-Support",
|
||||
"Erweiterte Tracking & Insights"
|
||||
]
|
||||
},
|
||||
"enterprise": {
|
||||
"title": "Enterprise",
|
||||
"name": "Enterprise",
|
||||
"price": "Individuell",
|
||||
"period": "",
|
||||
"features": [
|
||||
"∞ dynamische QR-Codes",
|
||||
"Unbegrenzte statische QR-Codes",
|
||||
"Alles aus Business",
|
||||
"Eigener Account Manager"
|
||||
],
|
||||
"contact": "Kontakt aufnehmen"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"title": "Häufig gestellte Fragen",
|
||||
"questions": {
|
||||
"account": {
|
||||
"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."
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"file_type": {
|
||||
"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."
|
||||
},
|
||||
"password": {
|
||||
"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": {
|
||||
"question": "Wie funktioniert die Analytik?",
|
||||
"answer": "Wir verfolgen Scans, Standorte, Geräte und Referrer unter Beachtung der Privatsphäre der Nutzer. Keine persönlichen Daten werden gespeichert."
|
||||
},
|
||||
"privacy": {
|
||||
"question": "Verfolgen Sie persönliche Daten?",
|
||||
"answer": "Wir respektieren die Privatsphäre und sammeln nur anonyme Nutzungsdaten. IP-Adressen werden gehasht und wir respektieren Do-Not-Track-Header."
|
||||
},
|
||||
"bulk": {
|
||||
"question": "Kann ich Codes in großen Mengen mit meinen eigenen Daten erstellen?",
|
||||
"answer": "Ja, Sie können CSV- oder Excel-Dateien hochladen, um mehrere QR-Codes auf einmal mit individueller Datenzuordnung zu erstellen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "Verwalten Sie Ihre QR-Codes und verfolgen Sie Ihre Performance",
|
||||
"stats": {
|
||||
"total_scans": "Gesamte Scans",
|
||||
"active_codes": "Aktive QR-Codes",
|
||||
"conversion_rate": "Konversionsrate"
|
||||
},
|
||||
"recent_codes": "Aktuelle QR-Codes",
|
||||
"blog_resources": "Blog & Ressourcen",
|
||||
"menu": {
|
||||
"edit": "Bearbeiten",
|
||||
"duplicate": "Duplizieren",
|
||||
"pause": "Pausieren",
|
||||
"delete": "Löschen"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "QR-Code erstellen",
|
||||
"subtitle": "Generieren Sie dynamische und statische QR-Codes mit individuellem Branding",
|
||||
"content": "Inhalt",
|
||||
"type": "QR-Code-Typ",
|
||||
"style": "Stil & Branding",
|
||||
"preview": "Live-Vorschau",
|
||||
"title_label": "Titel",
|
||||
"title_placeholder": "Mein QR-Code",
|
||||
"content_type": "Inhaltstyp",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://beispiel.de",
|
||||
"tags_label": "Tags (durch Komma getrennt)",
|
||||
"tags_placeholder": "marketing, kampagne, 2025",
|
||||
"qr_code_type": "QR-Code-Typ",
|
||||
"dynamic": "Dynamisch",
|
||||
"static": "Statisch",
|
||||
"recommended": "Empfohlen",
|
||||
"dynamic_description": "Dynamisch: Scans verfolgen, URL später bearbeiten, Analytik ansehen. QR enthält Tracking-Link.",
|
||||
"static_description": "Statisch: Direkt zum Inhalt, kein Tracking, nicht bearbeitbar. QR enthält tatsächlichen Inhalt.",
|
||||
"foreground_color": "Vordergrundfarbe",
|
||||
"background_color": "Hintergrundfarbe",
|
||||
"corner_style": "Eckenstil",
|
||||
"size": "Größe",
|
||||
"good_contrast": "Guter Kontrast",
|
||||
"contrast_ratio": "Kontrastverhältnis",
|
||||
"download_svg": "SVG herunterladen",
|
||||
"download_png": "PNG herunterladen",
|
||||
"save_qr_code": "QR-Code speichern"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytik",
|
||||
"subtitle": "Verfolgen und analysieren Sie die Performance Ihrer QR-Codes",
|
||||
"export_report": "Bericht exportieren",
|
||||
"from_last_period": "vom letzten Zeitraum",
|
||||
"no_mobile_scans": "Keine mobilen Scans",
|
||||
"of_total": "der Gesamtmenge",
|
||||
"ranges": {
|
||||
"7d": "7 Tage",
|
||||
"30d": "30 Tage",
|
||||
"90d": "90 Tage"
|
||||
},
|
||||
"kpis": {
|
||||
"total_scans": "Gesamte Scans",
|
||||
"avg_scans": "Ø Scans/QR",
|
||||
"mobile_usage": "Mobile Nutzung",
|
||||
"top_country": "Top Land"
|
||||
},
|
||||
"charts": {
|
||||
"scans_over_time": "Scans über Zeit",
|
||||
"device_types": "Gerätetypen",
|
||||
"top_countries": "Top Länder"
|
||||
},
|
||||
"table": {
|
||||
"qr_code": "QR-Code",
|
||||
"type": "Typ",
|
||||
"total_scans": "Gesamte Scans",
|
||||
"unique_scans": "Einzigartige Scans",
|
||||
"conversion": "Konversion",
|
||||
"trend": "Trend",
|
||||
"scans": "Scans",
|
||||
"percentage": "Prozent",
|
||||
"country": "Land",
|
||||
"performance": "Performance",
|
||||
"created": "Erstellt",
|
||||
"status": "Status"
|
||||
},
|
||||
"performance_title": "QR-Code-Performance"
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Massen-Erstellung",
|
||||
"subtitle": "Erstellen Sie mehrere QR-Codes gleichzeitig aus CSV- oder Excel-Dateien",
|
||||
"template_warning_title": "Bitte folgen Sie dem Vorlagenformat",
|
||||
"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.",
|
||||
"static_only_title": "Nur statische QR-Codes",
|
||||
"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",
|
||||
"no_file_selected": "Keine ausgewählt",
|
||||
"simple_format": "Einfaches Format",
|
||||
"just_title_url": "Nur Titel & URL",
|
||||
"static_qr_codes": "Statische QR-Codes",
|
||||
"no_tracking": "Kein Tracking enthalten",
|
||||
"instant_download": "Sofortiger Download",
|
||||
"get_zip": "ZIP mit allen SVGs erhalten",
|
||||
"max_rows": "max 1.000 Zeilen",
|
||||
"steps": {
|
||||
"upload": "Datei hochladen",
|
||||
"preview": "Vorschau & Zuordnung",
|
||||
"download": "Herunterladen"
|
||||
},
|
||||
"drag_drop": "Datei hier hinziehen",
|
||||
"or_click": "oder klicken zum Durchsuchen",
|
||||
"supported_formats": "Unterstützt CSV, XLS, XLSX (max 1.000 Zeilen)"
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrationen",
|
||||
"metrics": {
|
||||
"total_codes": "QR-Codes Gesamt",
|
||||
"active_integrations": "Aktive Integrationen",
|
||||
"sync_status": "Sync-Status",
|
||||
"available_services": "Verfügbare Services"
|
||||
},
|
||||
"zapier": {
|
||||
"title": "Zapier",
|
||||
"description": "Automatisieren Sie QR-Code-Erstellung mit 5000+ Apps",
|
||||
"features": [
|
||||
"Trigger bei neuen QR-Codes",
|
||||
"Codes aus anderen Apps erstellen",
|
||||
"Scan-Daten synchronisieren"
|
||||
]
|
||||
},
|
||||
"airtable": {
|
||||
"title": "Airtable",
|
||||
"description": "Synchronisieren Sie QR-Codes mit Ihren Airtable-Basen",
|
||||
"features": [
|
||||
"Bidirektionale Synchronisation",
|
||||
"Individuelle Feldzuordnung",
|
||||
"Echtzeit-Updates"
|
||||
]
|
||||
},
|
||||
"sheets": {
|
||||
"title": "Google Sheets",
|
||||
"description": "Exportieren Sie Daten automatisch zu Google Sheets",
|
||||
"features": [
|
||||
"Automatisierte Exporte",
|
||||
"Individuelle Vorlagen",
|
||||
"Geplante Updates"
|
||||
]
|
||||
},
|
||||
"activate": "Aktivieren & Konfigurieren"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"subtitle": "Verwalten Sie Ihre Kontoeinstellungen und Präferenzen",
|
||||
"tabs": {
|
||||
"profile": "Profil",
|
||||
"billing": "Abrechnung",
|
||||
"team": "Team & Rollen",
|
||||
"api": "API-Schlüssel",
|
||||
"workspace": "Arbeitsbereich"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"create": "Erstellen",
|
||||
"loading": "Lädt...",
|
||||
"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",
|
||||
"legal": "Rechtliches",
|
||||
"industries": "Branchen",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
|
||||
"newsletter": "Newsletter-Anmeldung",
|
||||
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
|
||||
}
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"tagline": "Erstellen Sie benutzerdefinierte QR-Codes in Sekunden mit erweitertem Tracking und Analysen.",
|
||||
"newsletter": "Newsletter-Anmeldung",
|
||||
"rights_reserved": "QR Master. Alle Rechte vorbehalten."
|
||||
}
|
||||
}
|
||||
|
||||
817
src/i18n/en.json
817
src/i18n/en.json
@@ -1,406 +1,411 @@
|
||||
{
|
||||
"nav": {
|
||||
"features": "Features",
|
||||
"pricing": "Pricing",
|
||||
"faq": "FAQ",
|
||||
"blog": "Blog",
|
||||
"login": "Login",
|
||||
"dashboard": "Dashboard",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"signup": "Sign Up",
|
||||
"learn": "Learn",
|
||||
"create_qr": "Create QR",
|
||||
"bulk_creation": "Bulk Creation",
|
||||
"analytics": "Analytics",
|
||||
"settings": "Settings",
|
||||
"cta": "Get Started Free",
|
||||
"tools": "Free Tools",
|
||||
"all_free": "All generators are 100% free",
|
||||
"resources": "Resources",
|
||||
"all_industries": "Industries"
|
||||
},
|
||||
"hero": {
|
||||
"badge": "Free QR Code Generator",
|
||||
"title": "Create QR Codes That Work Everywhere",
|
||||
"subtitle": "Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever.",
|
||||
"features": [
|
||||
"No credit card required to start",
|
||||
"Create QR codes free forever",
|
||||
"Advanced tracking and analytics",
|
||||
"Custom colors and styles"
|
||||
],
|
||||
"cta_primary": "Make a QR Code Free",
|
||||
"cta_secondary": "View Pricing",
|
||||
"engagement_badge": "Free Forever"
|
||||
},
|
||||
"trust": {
|
||||
"users": "Happy Users",
|
||||
"codes": "Active QR Codes",
|
||||
"scans": "Total Scans",
|
||||
"countries": "Countries"
|
||||
},
|
||||
"industries": {
|
||||
"restaurant": "Restaurant Chain",
|
||||
"tech": "Tech Startup",
|
||||
"realestate": "Real Estate",
|
||||
"events": "Event Agency",
|
||||
"retail": "Retail Store",
|
||||
"healthcare": "Healthcare"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Start with a Template",
|
||||
"restaurant": "Restaurant Menu",
|
||||
"business": "Business Card",
|
||||
"vcard": "Contact Card",
|
||||
"event": "Event Ticket",
|
||||
"use_template": "Use template →"
|
||||
},
|
||||
"generator": {
|
||||
"title": "Instant QR Code Generator",
|
||||
"url_placeholder": "Enter your URL here...",
|
||||
"foreground": "Foreground",
|
||||
"background": "Background",
|
||||
"corners": "Corners",
|
||||
"size": "Size",
|
||||
"contrast_good": "Good contrast",
|
||||
"download_svg": "Download SVG",
|
||||
"download_png": "Download PNG",
|
||||
"save_track": "Save & Track",
|
||||
"live_preview": "Live Preview",
|
||||
"demo_note": "This is a demo QR code"
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"title": "Why Dynamic QR Codes Save You Money",
|
||||
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
|
||||
"static": {
|
||||
"title": "Static QR Codes",
|
||||
"subtitle": "Always Free",
|
||||
"description": "Perfect for permanent content that never changes",
|
||||
"features": [
|
||||
"Content cannot be edited",
|
||||
"No scan tracking",
|
||||
"Works forever",
|
||||
"No account required"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"title": "Dynamic QR Codes",
|
||||
"subtitle": "Recommended",
|
||||
"description": "Full control with tracking and editing capabilities",
|
||||
"features": [
|
||||
"Edit content anytime",
|
||||
"Advanced analytics",
|
||||
"Custom branding",
|
||||
"Bulk operations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Everything you need to create professional QR codes",
|
||||
"analytics": {
|
||||
"title": "Advanced Analytics",
|
||||
"description": "Track scans, locations, devices, and user behavior with detailed insights."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Full Customization",
|
||||
"description": "Brand your QR codes with custom colors and styling options."
|
||||
},
|
||||
"unlimited": {
|
||||
"title": "Unlimited Static QR Codes",
|
||||
"description": "Create as many static QR codes as you need. Free forever, no limits."
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk Operations",
|
||||
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrations",
|
||||
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
|
||||
},
|
||||
"api": {
|
||||
"title": "Developer API",
|
||||
"description": "Integrate QR code generation into your applications with our REST API."
|
||||
},
|
||||
"support": {
|
||||
"title": "24/7 Support",
|
||||
"description": "Get help when you need it with our dedicated customer support team."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Choose Your Plan",
|
||||
"subtitle": "Select the perfect plan for your QR code needs",
|
||||
"choose_plan": "Choose Your Plan",
|
||||
"select_plan": "Select the perfect plan for your QR code needs",
|
||||
"current_plan": "Current Plan",
|
||||
"upgrade_to": "Upgrade to",
|
||||
"downgrade_to_free": "Downgrade to Free",
|
||||
"most_popular": "Most Popular",
|
||||
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
|
||||
"free": {
|
||||
"title": "Free",
|
||||
"name": "Free",
|
||||
"price": "€0",
|
||||
"period": "forever",
|
||||
"features": [
|
||||
"3 active dynamic QR codes (8 types available)",
|
||||
"Unlimited static QR codes",
|
||||
"Basic scan tracking",
|
||||
"Standard QR design templates",
|
||||
"Download as SVG/PNG"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
"title": "Pro",
|
||||
"name": "Pro",
|
||||
"price": "€9",
|
||||
"period": "per month",
|
||||
"badge": "Most Popular",
|
||||
"features": [
|
||||
"50 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Advanced analytics (scans, devices, locations)",
|
||||
"Custom branding (colors & logos)"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
"title": "Business",
|
||||
"name": "Business",
|
||||
"price": "€29",
|
||||
"period": "per month",
|
||||
"features": [
|
||||
"500 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Everything from Pro",
|
||||
"Bulk QR Creation (up to 1,000)",
|
||||
"Priority email support",
|
||||
"Advanced tracking & insights"
|
||||
]
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"title": "Frequently Asked Questions",
|
||||
"questions": {
|
||||
"account": {
|
||||
"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."
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"file_type": {
|
||||
"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."
|
||||
},
|
||||
"password": {
|
||||
"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": {
|
||||
"question": "How do analytics work?",
|
||||
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
|
||||
},
|
||||
"privacy": {
|
||||
"question": "Do you track personal data?",
|
||||
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
|
||||
},
|
||||
"bulk": {
|
||||
"question": "Can I bulk-create codes with my own data?",
|
||||
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "Manage your QR codes and track performance",
|
||||
"stats": {
|
||||
"total_scans": "Total Scans",
|
||||
"active_codes": "Active QR Codes",
|
||||
"conversion_rate": "Conversion Rate"
|
||||
},
|
||||
"recent_codes": "Recent QR Codes",
|
||||
"blog_resources": "Blog & Resources",
|
||||
"menu": {
|
||||
"edit": "Edit",
|
||||
"duplicate": "Duplicate",
|
||||
"pause": "Pause",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "Create QR Code",
|
||||
"subtitle": "Generate dynamic and static QR codes with custom branding",
|
||||
"content": "Content",
|
||||
"type": "QR Code Type",
|
||||
"style": "Style & Branding",
|
||||
"preview": "Live Preview",
|
||||
"title_label": "Title",
|
||||
"title_placeholder": "My QR Code",
|
||||
"content_type": "Content Type",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://example.com",
|
||||
"tags_label": "Tags (comma-separated)",
|
||||
"tags_placeholder": "marketing, campaign, 2025",
|
||||
"qr_code_type": "QR Code Type",
|
||||
"dynamic": "Dynamic",
|
||||
"static": "Static",
|
||||
"recommended": "Recommended",
|
||||
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
|
||||
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
|
||||
"foreground_color": "Foreground Color",
|
||||
"background_color": "Background Color",
|
||||
"corner_style": "Corner Style",
|
||||
"size": "Size",
|
||||
"good_contrast": "Good contrast",
|
||||
"contrast_ratio": "Contrast ratio",
|
||||
"download_svg": "Download SVG",
|
||||
"download_png": "Download PNG",
|
||||
"save_qr_code": "Save QR Code"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"subtitle": "Track and analyze your QR code performance",
|
||||
"export_report": "Export Report",
|
||||
"from_last_period": "from last period",
|
||||
"no_mobile_scans": "No mobile scans",
|
||||
"of_total": "of total",
|
||||
"ranges": {
|
||||
"7d": "7 Days",
|
||||
"30d": "30 Days",
|
||||
"90d": "90 Days"
|
||||
},
|
||||
"kpis": {
|
||||
"total_scans": "Total Scans",
|
||||
"avg_scans": "Avg Scans/QR",
|
||||
"mobile_usage": "Mobile Usage",
|
||||
"top_country": "Top Country"
|
||||
},
|
||||
"charts": {
|
||||
"scans_over_time": "Scans Over Time",
|
||||
"device_types": "Device Types",
|
||||
"top_countries": "Top Countries"
|
||||
},
|
||||
"table": {
|
||||
"qr_code": "QR Code",
|
||||
"type": "Type",
|
||||
"total_scans": "Total Scans",
|
||||
"unique_scans": "Unique Scans",
|
||||
"conversion": "Conversion",
|
||||
"trend": "Trend",
|
||||
"scans": "Scans",
|
||||
"percentage": "Percentage",
|
||||
"country": "Country",
|
||||
"performance": "Performance",
|
||||
"created": "Created",
|
||||
"status": "Status"
|
||||
},
|
||||
"performance_title": "QR Code Performance"
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk Creation",
|
||||
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
|
||||
"template_warning_title": "Please Follow the Template Format",
|
||||
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
|
||||
"static_only_title": "Static QR Codes Only",
|
||||
"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",
|
||||
"no_file_selected": "No file selected",
|
||||
"simple_format": "Simple Format",
|
||||
"just_title_url": "Just title & URL",
|
||||
"static_qr_codes": "Static QR Codes",
|
||||
"no_tracking": "No tracking included",
|
||||
"instant_download": "Instant Download",
|
||||
"get_zip": "Get ZIP with all SVGs",
|
||||
"max_rows": "max 1,000 rows",
|
||||
"steps": {
|
||||
"upload": "Upload File",
|
||||
"preview": "Preview & Map",
|
||||
"download": "Download"
|
||||
},
|
||||
"drag_drop": "Drag & drop your file here",
|
||||
"or_click": "or click to browse",
|
||||
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrations",
|
||||
"metrics": {
|
||||
"total_codes": "QR Codes Total",
|
||||
"active_integrations": "Active Integrations",
|
||||
"sync_status": "Sync Status",
|
||||
"available_services": "Available Services"
|
||||
},
|
||||
"zapier": {
|
||||
"title": "Zapier",
|
||||
"description": "Automate QR code creation with 5000+ apps",
|
||||
"features": [
|
||||
"Trigger on new QR codes",
|
||||
"Create codes from other apps",
|
||||
"Sync scan data"
|
||||
]
|
||||
},
|
||||
"airtable": {
|
||||
"title": "Airtable",
|
||||
"description": "Sync QR codes with your Airtable bases",
|
||||
"features": [
|
||||
"Two-way sync",
|
||||
"Custom field mapping",
|
||||
"Real-time updates"
|
||||
]
|
||||
},
|
||||
"sheets": {
|
||||
"title": "Google Sheets",
|
||||
"description": "Export data to Google Sheets automatically",
|
||||
"features": [
|
||||
"Automated exports",
|
||||
"Custom templates",
|
||||
"Scheduled updates"
|
||||
]
|
||||
},
|
||||
"activate": "Activate & Configure"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"subtitle": "Manage your account settings and preferences",
|
||||
"tabs": {
|
||||
"profile": "Profile",
|
||||
"billing": "Billing",
|
||||
"team": "Team & Roles",
|
||||
"api": "API Keys",
|
||||
"workspace": "Workspace"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"success": "Success!"
|
||||
},
|
||||
"footer": {
|
||||
"product": "Product",
|
||||
"features": "Features",
|
||||
"pricing": "Pricing",
|
||||
"faq": "FAQ",
|
||||
"blog": "Blog",
|
||||
"resources": "Resources",
|
||||
"full_pricing": "Full Pricing",
|
||||
"all_questions": "All Questions",
|
||||
"all_articles": "All Articles",
|
||||
"learn": "Learn",
|
||||
"get_started": "Get Started",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
{
|
||||
"nav": {
|
||||
"features": "Features",
|
||||
"pricing": "Pricing",
|
||||
"faq": "FAQ",
|
||||
"blog": "Blog",
|
||||
"login": "Login",
|
||||
"dashboard": "Dashboard",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"signup": "Sign Up",
|
||||
"learn": "Learn",
|
||||
"create_qr": "Create QR",
|
||||
"bulk_creation": "Bulk Creation",
|
||||
"analytics": "Analytics",
|
||||
"settings": "Settings",
|
||||
"cta": "Get Started Free",
|
||||
"tools": "Free Tools",
|
||||
"all_free": "All generators are 100% free",
|
||||
"resources": "Resources",
|
||||
"all_industries": "Industries"
|
||||
},
|
||||
"hero": {
|
||||
"badge": "Free QR Code Generator",
|
||||
"title": "Create QR Codes That Work Everywhere",
|
||||
"subtitle": "Generate static and dynamic QR codes with tracking, custom branding, and bulk generation. Free forever.",
|
||||
"features": [
|
||||
"No credit card required to start",
|
||||
"Create QR codes free forever",
|
||||
"Advanced tracking and analytics",
|
||||
"Custom colors and styles"
|
||||
],
|
||||
"cta_primary": "Make a QR Code Free",
|
||||
"cta_secondary": "View Pricing",
|
||||
"engagement_badge": "Free Forever"
|
||||
},
|
||||
"trust": {
|
||||
"users": "Happy Users",
|
||||
"codes": "Active QR Codes",
|
||||
"scans": "Total Scans",
|
||||
"countries": "Countries"
|
||||
},
|
||||
"industries": {
|
||||
"restaurant": "Restaurant Chain",
|
||||
"tech": "Tech Startup",
|
||||
"realestate": "Real Estate",
|
||||
"events": "Event Agency",
|
||||
"retail": "Retail Store",
|
||||
"healthcare": "Healthcare"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Start with a Template",
|
||||
"restaurant": "Restaurant Menu",
|
||||
"business": "Business Card",
|
||||
"vcard": "Contact Card",
|
||||
"event": "Event Ticket",
|
||||
"use_template": "Use template →"
|
||||
},
|
||||
"generator": {
|
||||
"title": "Instant QR Code Generator",
|
||||
"url_placeholder": "Enter your URL here...",
|
||||
"foreground": "Foreground",
|
||||
"background": "Background",
|
||||
"corners": "Corners",
|
||||
"size": "Size",
|
||||
"contrast_good": "Good contrast",
|
||||
"download_svg": "Download SVG",
|
||||
"download_png": "Download PNG",
|
||||
"save_track": "Save & Track",
|
||||
"live_preview": "Live Preview",
|
||||
"demo_note": "This is a demo QR code"
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"title": "Why Dynamic QR Codes Save You Money",
|
||||
"description": "Stop re-printing materials. Switch destinations instantly and track every scan.",
|
||||
"static": {
|
||||
"title": "Static QR Codes",
|
||||
"subtitle": "Always Free",
|
||||
"description": "Perfect for permanent content that never changes",
|
||||
"features": [
|
||||
"Content cannot be edited",
|
||||
"No scan tracking",
|
||||
"Works forever",
|
||||
"No account required"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"title": "Dynamic QR Codes",
|
||||
"subtitle": "Recommended",
|
||||
"description": "Full control with tracking and editing capabilities",
|
||||
"features": [
|
||||
"Edit content anytime",
|
||||
"Advanced analytics",
|
||||
"Custom branding",
|
||||
"Bulk operations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Everything you need to create professional QR codes",
|
||||
"analytics": {
|
||||
"title": "Advanced Analytics",
|
||||
"description": "Track scans, locations, devices, and user behavior with detailed insights."
|
||||
},
|
||||
"customization": {
|
||||
"title": "Full Customization",
|
||||
"description": "Brand your QR codes with custom colors and styling options."
|
||||
},
|
||||
"unlimited": {
|
||||
"title": "Unlimited Static QR Codes",
|
||||
"description": "Create as many static QR codes as you need. Free forever, no limits."
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk Operations",
|
||||
"description": "Create hundreds of QR codes at once with CSV import and batch processing."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrations",
|
||||
"description": "Connect with Zapier, Airtable, Google Sheets, and more popular tools."
|
||||
},
|
||||
"api": {
|
||||
"title": "Developer API",
|
||||
"description": "Integrate QR code generation into your applications with our REST API."
|
||||
},
|
||||
"support": {
|
||||
"title": "24/7 Support",
|
||||
"description": "Get help when you need it with our dedicated customer support team."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Choose Your Plan",
|
||||
"subtitle": "Select the perfect plan for your QR code needs",
|
||||
"choose_plan": "Choose Your Plan",
|
||||
"select_plan": "Select the perfect plan for your QR code needs",
|
||||
"current_plan": "Current Plan",
|
||||
"upgrade_to": "Upgrade to",
|
||||
"downgrade_to_free": "Downgrade to Free",
|
||||
"most_popular": "Most Popular",
|
||||
"all_plans_note": "All plans include unlimited static QR codes and basic customization.",
|
||||
"free": {
|
||||
"title": "Free",
|
||||
"name": "Free",
|
||||
"price": "€0",
|
||||
"period": "forever",
|
||||
"features": [
|
||||
"3 active dynamic QR codes (8 types available)",
|
||||
"Unlimited static QR codes",
|
||||
"Basic scan tracking",
|
||||
"Standard QR design templates",
|
||||
"Download as SVG/PNG"
|
||||
]
|
||||
},
|
||||
"pro": {
|
||||
"title": "Pro",
|
||||
"name": "Pro",
|
||||
"price": "€9",
|
||||
"period": "per month",
|
||||
"badge": "Most Popular",
|
||||
"features": [
|
||||
"50 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Advanced analytics (scans, devices, locations)",
|
||||
"Custom branding (colors & logos)"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
"title": "Business",
|
||||
"name": "Business",
|
||||
"price": "€29",
|
||||
"period": "per month",
|
||||
"features": [
|
||||
"500 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Everything from Pro",
|
||||
"Bulk QR Creation (up to 1,000)",
|
||||
"Priority email support",
|
||||
"Advanced tracking & insights"
|
||||
]
|
||||
},
|
||||
"enterprise": {
|
||||
"title": "Enterprise",
|
||||
"name": "Enterprise",
|
||||
"price": "Custom",
|
||||
"period": "",
|
||||
"features": [
|
||||
"∞ dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Everything from Business",
|
||||
"Dedicated Account Manager"
|
||||
],
|
||||
"contact": "Contact Us"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"title": "Frequently Asked Questions",
|
||||
"questions": {
|
||||
"account": {
|
||||
"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."
|
||||
},
|
||||
"static_vs_dynamic": {
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"file_type": {
|
||||
"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."
|
||||
},
|
||||
"password": {
|
||||
"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": {
|
||||
"question": "How do analytics work?",
|
||||
"answer": "We track scans, locations, devices, and referrers while respecting user privacy. No personal data is stored."
|
||||
},
|
||||
"privacy": {
|
||||
"question": "Do you track personal data?",
|
||||
"answer": "We respect privacy and only collect anonymous usage data. IP addresses are hashed and we honor Do Not Track headers."
|
||||
},
|
||||
"bulk": {
|
||||
"question": "Can I bulk-create codes with my own data?",
|
||||
"answer": "Yes, you can upload CSV or Excel files to create multiple QR codes at once with custom data mapping."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "Manage your QR codes and track performance",
|
||||
"stats": {
|
||||
"total_scans": "Total Scans",
|
||||
"active_codes": "Active QR Codes",
|
||||
"conversion_rate": "Conversion Rate"
|
||||
},
|
||||
"recent_codes": "Recent QR Codes",
|
||||
"blog_resources": "Blog & Resources",
|
||||
"menu": {
|
||||
"edit": "Edit",
|
||||
"duplicate": "Duplicate",
|
||||
"pause": "Pause",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "Create QR Code",
|
||||
"subtitle": "Generate dynamic and static QR codes with custom branding",
|
||||
"content": "Content",
|
||||
"type": "QR Code Type",
|
||||
"style": "Style & Branding",
|
||||
"preview": "Live Preview",
|
||||
"title_label": "Title",
|
||||
"title_placeholder": "My QR Code",
|
||||
"content_type": "Content Type",
|
||||
"url_label": "URL",
|
||||
"url_placeholder": "https://example.com",
|
||||
"tags_label": "Tags (comma-separated)",
|
||||
"tags_placeholder": "marketing, campaign, 2025",
|
||||
"qr_code_type": "QR Code Type",
|
||||
"dynamic": "Dynamic",
|
||||
"static": "Static",
|
||||
"recommended": "Recommended",
|
||||
"dynamic_description": "Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.",
|
||||
"static_description": "Static: Direct to content, no tracking, cannot edit. QR contains actual content.",
|
||||
"foreground_color": "Foreground Color",
|
||||
"background_color": "Background Color",
|
||||
"corner_style": "Corner Style",
|
||||
"size": "Size",
|
||||
"good_contrast": "Good contrast",
|
||||
"contrast_ratio": "Contrast ratio",
|
||||
"download_svg": "Download SVG",
|
||||
"download_png": "Download PNG",
|
||||
"save_qr_code": "Save QR Code"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"subtitle": "Track and analyze your QR code performance",
|
||||
"export_report": "Export Report",
|
||||
"from_last_period": "from last period",
|
||||
"no_mobile_scans": "No mobile scans",
|
||||
"of_total": "of total",
|
||||
"ranges": {
|
||||
"7d": "7 Days",
|
||||
"30d": "30 Days",
|
||||
"90d": "90 Days"
|
||||
},
|
||||
"kpis": {
|
||||
"total_scans": "Total Scans",
|
||||
"avg_scans": "Avg Scans/QR",
|
||||
"mobile_usage": "Mobile Usage",
|
||||
"top_country": "Top Country"
|
||||
},
|
||||
"charts": {
|
||||
"scans_over_time": "Scans Over Time",
|
||||
"device_types": "Device Types",
|
||||
"top_countries": "Top Countries"
|
||||
},
|
||||
"table": {
|
||||
"qr_code": "QR Code",
|
||||
"type": "Type",
|
||||
"total_scans": "Total Scans",
|
||||
"unique_scans": "Unique Scans",
|
||||
"conversion": "Conversion",
|
||||
"trend": "Trend",
|
||||
"scans": "Scans",
|
||||
"percentage": "Percentage",
|
||||
"country": "Country",
|
||||
"performance": "Performance",
|
||||
"created": "Created",
|
||||
"status": "Status"
|
||||
},
|
||||
"performance_title": "QR Code Performance"
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk Creation",
|
||||
"subtitle": "Create multiple QR codes at once from CSV or Excel files",
|
||||
"template_warning_title": "Please Follow the Template Format",
|
||||
"template_warning_text": "Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).",
|
||||
"static_only_title": "Static QR Codes Only",
|
||||
"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",
|
||||
"no_file_selected": "No file selected",
|
||||
"simple_format": "Simple Format",
|
||||
"just_title_url": "Just title & URL",
|
||||
"static_qr_codes": "Static QR Codes",
|
||||
"no_tracking": "No tracking included",
|
||||
"instant_download": "Instant Download",
|
||||
"get_zip": "Get ZIP with all SVGs",
|
||||
"max_rows": "max 1,000 rows",
|
||||
"steps": {
|
||||
"upload": "Upload File",
|
||||
"preview": "Preview & Map",
|
||||
"download": "Download"
|
||||
},
|
||||
"drag_drop": "Drag & drop your file here",
|
||||
"or_click": "or click to browse",
|
||||
"supported_formats": "Supports CSV, XLS, XLSX (max 1,000 rows)"
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrations",
|
||||
"metrics": {
|
||||
"total_codes": "QR Codes Total",
|
||||
"active_integrations": "Active Integrations",
|
||||
"sync_status": "Sync Status",
|
||||
"available_services": "Available Services"
|
||||
},
|
||||
"zapier": {
|
||||
"title": "Zapier",
|
||||
"description": "Automate QR code creation with 5000+ apps",
|
||||
"features": [
|
||||
"Trigger on new QR codes",
|
||||
"Create codes from other apps",
|
||||
"Sync scan data"
|
||||
]
|
||||
},
|
||||
"airtable": {
|
||||
"title": "Airtable",
|
||||
"description": "Sync QR codes with your Airtable bases",
|
||||
"features": ["Two-way sync", "Custom field mapping", "Real-time updates"]
|
||||
},
|
||||
"sheets": {
|
||||
"title": "Google Sheets",
|
||||
"description": "Export data to Google Sheets automatically",
|
||||
"features": ["Automated exports", "Custom templates", "Scheduled updates"]
|
||||
},
|
||||
"activate": "Activate & Configure"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"subtitle": "Manage your account settings and preferences",
|
||||
"tabs": {
|
||||
"profile": "Profile",
|
||||
"billing": "Billing",
|
||||
"team": "Team & Roles",
|
||||
"api": "API Keys",
|
||||
"workspace": "Workspace"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"success": "Success!"
|
||||
},
|
||||
"footer": {
|
||||
"product": "Product",
|
||||
"features": "Features",
|
||||
"pricing": "Pricing",
|
||||
"faq": "FAQ",
|
||||
"blog": "Blog",
|
||||
"resources": "Resources",
|
||||
"full_pricing": "Full Pricing",
|
||||
"all_questions": "All Questions",
|
||||
"all_articles": "All Articles",
|
||||
"learn": "Learn",
|
||||
"get_started": "Get Started",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function middleware(req: NextRequest) {
|
||||
// '/guide', // Redirected to /learn/*
|
||||
'/qr-code-erstellen',
|
||||
'/dynamic-qr-code-generator',
|
||||
'/dynamic-barcode-generator',
|
||||
'/bulk-qr-code-generator',
|
||||
'/qr-code-tracking',
|
||||
'/qr-code-analytics',
|
||||
|
||||
Reference in New Issue
Block a user