Paar fehler

This commit is contained in:
knuthtimo-lab 2026-01-22 20:09:54 +01:00
parent e44dc1c6bb
commit 59131a54f0
26 changed files with 1609 additions and 1544 deletions

24
features_overview_de.md Normal file
View File

@ -0,0 +1,24 @@
# Neue Features und Updates für QR Master (DE)
## Übersicht
Wir haben unser Angebot aktualisiert, um noch mehr Wert für unsere Nutzer zu bieten. Hier sind die neuesten Ergänzungen und Verbesserungen:
### 1. Erweiterte QR Code Typen
Wir haben spezifische QR Code Lösungen für verschiedene Anwendungsfälle hinzugefügt:
- **Feedback QR Code**: Sammeln Sie direkt Kundenfeedback. Scans führen zu einem anpassbaren Feedback-Formular.
- **PDF QR Code**: Teilen Sie Dokumente, Speisekarten oder Broschüren als PDF. Ideal für Restaurants und Unternehmen.
- **Coupon QR Code**: Bieten Sie Rabatte und Gutscheine via QR Code an. Perfekt für Marketingkampagnen im Einzelhandel.
- **App Store QR Code**: Ein intelligenter QR Code, der Nutzer basierend auf ihrem Gerät (iOS oder Android) automatisch zum richtigen App Store leitet.
### 2. Mehr Dynamik im Kostenlosen Plan
Um den Einstieg zu erleichtern, haben wir das Limit für den kostenlosen Plan erhöht:
- **Neu**: 8 Dynamische QR Codes kostenlos (statt bisher 3).
- **Vorteil**: Mehr Flexibilität für kleine Unternehmen und Startups, um verschiedene Kampagnen gleichzeitig zu testen.
### 3. SEO Optimierung
Alle neuen QR Code Typen sind jetzt vollständig in unsere Plattform integriert und für Suchmaschinen optimiert, damit Nutzer die richtige Lösung für ihr Problem finden.
---
*Erstellt am 22.01.2026*

View File

@ -17,22 +17,16 @@ export const metadata: Metadata = {
}, },
}; };
export default function RootAppLayout({ export default function AppGroupLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en">
<body className="font-sans">
<Providers>
<Suspense fallback={null}> <Suspense fallback={null}>
<AppLayout> <AppLayout>
{children} {children}
</AppLayout> </AppLayout>
</Suspense> </Suspense>
</Providers>
</body>
</html>
); );
} }

View File

@ -47,14 +47,13 @@ export const metadata: Metadata = {
}, },
}; };
export default function RootMarketingLayout({ export default function MarketingGroupLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <>
<head>
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
@ -63,14 +62,9 @@ export default function RootMarketingLayout({
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/> />
</head>
<body className="font-sans">
<Providers>
<MarketingLayout> <MarketingLayout>
{children} {children}
</MarketingLayout> </MarketingLayout>
</Providers> </>
</body>
</html>
); );
} }

View File

@ -10,6 +10,7 @@ import { Select } from '@/components/ui/Select';
import { showToast } from '@/components/ui/Toast'; import { showToast } from '@/components/ui/Toast';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { toPng, toSvg, toBlob } from 'html-to-image'; import { toPng, toSvg, toBlob } from 'html-to-image';
import { trackEvent } from '@/components/PostHogProvider';
// Brand Colors // Brand Colors
const BRAND = { const BRAND = {
@ -70,7 +71,19 @@ export default function BarcodeGeneratorClient() {
} else if ((format === 'ITF14' || format === 'MSI') && !/^\d+$/.test(value)) { } else if ((format === 'ITF14' || format === 'MSI') && !/^\d+$/.test(value)) {
setError('This format only supports numbers.'); setError('This format only supports numbers.');
} }
}, [value, format]);
if (value && !error) {
trackEvent('barcode_generated', {
format: format,
content_length: value.length,
width: width,
height: height,
display_value: displayValue,
line_color: lineColor,
frame_type: frameType
});
}
}, [value, format, width, height, displayValue, lineColor, frameType, error]);
const downloadBarcode = async (extension: 'png' | 'svg') => { const downloadBarcode = async (extension: 'png' | 'svg') => {
if (!barcodeRef.current) return; if (!barcodeRef.current) return;
@ -96,6 +109,11 @@ export default function BarcodeGeneratorClient() {
document.body.removeChild(link); document.body.removeChild(link);
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success'); showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
trackEvent('barcode_downloaded', {
format: format,
extension: extension,
frame_type: frameType
});
} catch (err) { } catch (err) {
console.error('Download failed', err); console.error('Download failed', err);
showToast('Download failed', 'error'); showToast('Download failed', 'error');
@ -121,6 +139,10 @@ export default function BarcodeGeneratorClient() {
}), }),
]); ]);
showToast('Barcode copied to clipboard', 'success'); showToast('Barcode copied to clipboard', 'success');
trackEvent('barcode_copied', {
format: format,
frame_type: frameType
});
} catch (err) { } catch (err) {
console.error('Copy failed', err); console.error('Copy failed', err);
showToast('Failed to copy barcode', 'error'); showToast('Failed to copy barcode', 'error');

View File

@ -9,7 +9,7 @@ export const metadata: Metadata = {
default: 'QR Master QR Code Generator & Analytics', default: 'QR Master QR Code Generator & Analytics',
template: '%s | QR Master', template: '%s | QR Master',
}, },
description: 'Erstellen Sie dynamische QR Codes, verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.', description: 'Erstellen Sie dynamische QR Codes für Feedback, PDF, Coupons und App Stores. Verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
metadataBase: new URL('https://www.qrmaster.net'), metadataBase: new URL('https://www.qrmaster.net'),
icons: { icons: {
icon: [ icon: [
@ -22,7 +22,7 @@ export const metadata: Metadata = {
type: 'website', type: 'website',
siteName: 'QR Master', siteName: 'QR Master',
title: 'QR Master QR Code Generator & Analytics', title: 'QR Master QR Code Generator & Analytics',
description: 'Erstellen Sie dynamische QR Codes, verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.', description: 'Erstellen Sie dynamische QR Codes für Feedback, PDF, Coupons und App Stores. Verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
url: 'https://www.qrmaster.net/qr-code-erstellen', url: 'https://www.qrmaster.net/qr-code-erstellen',
locale: 'de_DE', locale: 'de_DE',
images: [ images: [
@ -42,14 +42,13 @@ export const metadata: Metadata = {
robots: { index: true, follow: true }, robots: { index: true, follow: true },
}; };
export default function RootMarketingDeLayout({ export default function MarketingDeGroupLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="de"> <>
<head>
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
@ -58,14 +57,9 @@ export default function RootMarketingDeLayout({
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/> />
</head>
<body className="font-sans">
<Providers>
<MarketingDeLayout> <MarketingDeLayout>
{children} {children}
</MarketingDeLayout> </MarketingDeLayout>
</Providers> </>
</body>
</html>
); );
} }

View File

@ -22,7 +22,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const title = 'QR Code Erstellen Kostenlos | QR Master'; const title = 'QR Code Erstellen Kostenlos | QR Master';
const description = 'Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.'; const description = 'Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes für Feedback, PDF, Coupons & App Stores. Mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.';
return { return {
title: { title: {
@ -37,7 +37,11 @@ export async function generateMetadata(): Promise<Metadata> {
'qr codes erstellen', 'qr codes erstellen',
'qr code erstellen kostenlos', 'qr code erstellen kostenlos',
'dynamischer qr code', 'dynamischer qr code',
'qr code mit logo' 'qr code mit logo',
'feedback qr code',
'pdf qr code',
'coupon qr code',
'app store qr code'
], ],
alternates: { alternates: {
canonical: 'https://www.qrmaster.net/qr-code-erstellen', canonical: 'https://www.qrmaster.net/qr-code-erstellen',
@ -84,7 +88,7 @@ export default function QRCodeErstellenPage() {
<div className="sr-only" aria-hidden="false"> <div className="sr-only" aria-hidden="false">
<p> <p>
Erstellen Sie professionelle QR Codes für Ihr Unternehmen mit QR Master. Unser dynamischer QR Code Generator Erstellen Sie professionelle QR Codes für Ihr Unternehmen mit QR Master. Unser dynamischer QR Code Generator
ermöglicht es Ihnen, trackbare QR Codes zu erstellen, Ziel-URLs jederzeit zu ändern und detaillierte Statistiken einzusehen. ermöglicht es Ihnen, trackbare QR Codes für Feedback, PDF-Dateien, Coupons und App Stores zu erstellen, Ziel-URLs jederzeit zu ändern und detaillierte Statistiken einzusehen.
Perfekt für Restaurants, Einzelhandel, Events und Marketing-Kampagnen. Perfekt für Restaurants, Einzelhandel, Events und Marketing-Kampagnen.
</p> </p>
<p> <p>
@ -93,7 +97,7 @@ export default function QRCodeErstellenPage() {
vCard QR Codes für digitale Visitenkarten und QR Codes für Restaurant-Speisekarten. vCard QR Codes für digitale Visitenkarten und QR Codes für Restaurant-Speisekarten.
</p> </p>
<p> <p>
Starten Sie kostenlos mit 3 dynamischen QR Codes und unbegrenzten statischen Codes. Upgrade auf Pro für 50 Codes Starten Sie kostenlos mit 8 dynamischen QR Codes und unbegrenzten statischen Codes. Upgrade auf Pro für 50 Codes
mit erweiterten Statistiken, oder Business für 500 Codes mit Massen-Erstellung und Prioritäts-Support. mit erweiterten Statistiken, oder Business für 500 Codes mit Massen-Erstellung und Prioritäts-Support.
</p> </p>
</div> </div>

View File

@ -142,7 +142,7 @@ export default function CouponPage() {
{/* Redeem Button */} {/* Redeem Button */}
{coupon.redeemUrl && !isExpired && ( {coupon.redeemUrl && !isExpired && (
<a <a
href={coupon.redeemUrl} href={ensureAbsoluteUrl(coupon.redeemUrl)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block w-full py-4 rounded-xl font-semibold text-center bg-gradient-to-r from-[#076653] to-[#0C342C] text-white hover:from-[#087861] hover:to-[#0E4036] transition-all shadow-lg shadow-emerald-200" className="block w-full py-4 rounded-xl font-semibold text-center bg-gradient-to-r from-[#076653] to-[#0C342C] text-white hover:from-[#087861] hover:to-[#0E4036] transition-all shadow-lg shadow-emerald-200"
@ -165,3 +165,11 @@ export default function CouponPage() {
</div> </div>
); );
} }
function ensureAbsoluteUrl(url: string | undefined): string | undefined {
if (!url) return undefined;
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
return `https://${url}`;
}

View File

@ -55,7 +55,8 @@ export default function FeedbackPage() {
if (rating >= 4 && feedback?.googleReviewUrl) { if (rating >= 4 && feedback?.googleReviewUrl) {
setTimeout(() => { setTimeout(() => {
window.location.href = feedback.googleReviewUrl!; const url = ensureAbsoluteUrl(feedback.googleReviewUrl);
if (url) window.location.href = url;
}, 2000); }, 2000);
} }
} catch (error) { } catch (error) {
@ -119,9 +120,9 @@ export default function FeedbackPage() {
{/* Card */} {/* Card */}
<div className="bg-white rounded-3xl shadow-xl overflow-hidden"> <div className="bg-white rounded-3xl shadow-xl overflow-hidden">
{/* Colored Header */} {/* Colored Header */}
<div className="bg-gradient-to-r from-[#4C5F4E] via-[#C6C0B3] to-[#FAF8F5] p-8 text-center"> <div className="bg-gradient-to-r from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] p-8 text-center">
<div className="w-14 h-14 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4"> <div className="w-14 h-14 bg-[#4C5F4E]/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Star className="w-7 h-7 text-white" /> <Star className="w-7 h-7 text-[#4C5F4E]" />
</div> </div>
<h1 className="text-2xl font-bold mb-1 text-gray-900">How was your experience?</h1> <h1 className="text-2xl font-bold mb-1 text-gray-900">How was your experience?</h1>
<p className="text-gray-700">{feedback.businessName}</p> <p className="text-gray-700">{feedback.businessName}</p>
@ -193,3 +194,11 @@ export default function FeedbackPage() {
</div> </div>
); );
} }
function ensureAbsoluteUrl(url: string | undefined): string | undefined {
if (!url) return undefined;
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
return `https://${url}`;
}

View File

@ -1,10 +1,7 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
import '@/styles/globals.css'; import '@/styles/globals.css';
import { ToastContainer } from '@/components/ui/Toast'; import { Providers } from '@/components/Providers';
import AuthProvider from '@/components/SessionProvider';
import { PostHogProvider } from '@/components/PostHogProvider';
import CookieBanner from '@/components/CookieBanner';
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true'; const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
@ -58,13 +55,9 @@ export default function RootLayout({
<html lang="en"> <html lang="en">
<body className="font-sans"> <body className="font-sans">
<Suspense fallback={null}> <Suspense fallback={null}>
<PostHogProvider> <Providers>
<AuthProvider>
{children} {children}
</AuthProvider> </Providers>
<CookieBanner />
<ToastContainer />
</PostHogProvider>
</Suspense> </Suspense>
</body> </body>
</html> </html>

View File

@ -32,7 +32,7 @@ export async function GET(
switch (qrCode.contentType) { switch (qrCode.contentType) {
case 'URL': case 'URL':
destination = content.url || 'https://example.com'; destination = ensureAbsoluteUrl(content.url);
break; break;
case 'PHONE': case 'PHONE':
destination = `tel:${content.phone}`; destination = `tel:${content.phone}`;
@ -61,7 +61,7 @@ export async function GET(
break; break;
case 'PDF': case 'PDF':
// Direct link to file // Direct link to file
destination = content.fileUrl || 'https://example.com/file.pdf'; destination = ensureAbsoluteUrl(content.fileUrl);
break; break;
case 'APP': case 'APP':
// Smart device detection for app stores // Smart device detection for app stores
@ -70,11 +70,11 @@ export async function GET(
const isAndroid = /android/i.test(userAgent); const isAndroid = /android/i.test(userAgent);
if (isIOS && content.iosUrl) { if (isIOS && content.iosUrl) {
destination = content.iosUrl; destination = ensureAbsoluteUrl(content.iosUrl);
} else if (isAndroid && content.androidUrl) { } else if (isAndroid && content.androidUrl) {
destination = content.androidUrl; destination = ensureAbsoluteUrl(content.androidUrl);
} else { } else {
destination = content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com'; destination = ensureAbsoluteUrl(content.fallbackUrl || content.iosUrl || content.androidUrl);
} }
break; break;
case 'COUPON': case 'COUPON':
@ -234,7 +234,16 @@ async function trackScan(qrId: string, request: NextRequest) {
}, },
}); });
} catch (error) { } catch (error) {
console.error('Error tracking scan:', error);
// Don't throw - this is fire and forget // Don't throw - this is fire and forget
} }
} }
function ensureAbsoluteUrl(url: string): string {
if (!url) return 'https://example.com';
// Check if it already has a protocol (http://, https://, myapp://, mailto:, etc.)
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
// Default to https for web URLs
return `https://${url}`;
}

View File

@ -4,17 +4,36 @@ import { useEffect, useState, useRef } from 'react';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
export function PostHogProvider({ children }: { children: React.ReactNode }) { import { Suspense } from 'react';
export function PostHogPageView() {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [isInitialized, setIsInitialized] = useState(false);
const initializationAttempted = useRef(false); useEffect(() => {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && pathname && (posthog as any)._loaded) {
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}
}, [pathname, searchParams]);
return null;
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const [initializationAttempted, setInitializationAttempted] = useState(false);
// Initialize PostHog once // Initialize PostHog once
useEffect(() => { useEffect(() => {
// Prevent double initialization in React Strict Mode if (initializationAttempted) return;
if (initializationAttempted.current) return; setInitializationAttempted(true);
initializationAttempted.current = true;
const cookieConsent = localStorage.getItem('cookieConsent'); const cookieConsent = localStorage.getItem('cookieConsent');
@ -27,50 +46,32 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
return; return;
} }
// Check if already initialized (using _loaded property)
if (!(posthog as any)._loaded) { if (!(posthog as any)._loaded) {
posthog.init(apiKey, { posthog.init(apiKey, {
api_host: apiHost || 'https://us.i.posthog.com', api_host: apiHost || 'https://us.i.posthog.com',
person_profiles: 'identified_only', person_profiles: 'identified_only',
capture_pageview: false, // Manual pageview tracking capture_pageview: false,
capture_pageleave: true, capture_pageleave: true,
autocapture: true, autocapture: true,
respect_dnt: true, respect_dnt: true,
opt_out_capturing_by_default: false, opt_out_capturing_by_default: false,
}); });
// Enable debug mode in development
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
posthog.debug(); posthog.debug();
} }
// Set initialized immediately after init
setIsInitialized(true);
} else {
setIsInitialized(true); // Already loaded
} }
} }
}, [initializationAttempted]);
// NO cleanup function - PostHog should persist across page navigation return (
}, []); <>
<Suspense fallback={null}>
// Track page views ONLY after PostHog is initialized <PostHogPageView />
useEffect(() => { </Suspense>
const cookieConsent = localStorage.getItem('cookieConsent'); {children}
</>
if (cookieConsent === 'accepted' && pathname && isInitialized) { );
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}
}, [pathname, searchParams, isInitialized]); // Added isInitialized dependency
return <>{children}</>;
} }
/** /**

View File

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

View File

@ -8,6 +8,8 @@ import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { calculateContrast } from '@/lib/utils'; import { calculateContrast } from '@/lib/utils';
import { trackEvent } from '@/components/PostHogProvider';
import { useEffect } from 'react';
interface InstantGeneratorProps { interface InstantGeneratorProps {
t: any; // i18n translation function t: any; // i18n translation function
@ -20,6 +22,18 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
const [cornerStyle, setCornerStyle] = useState('square'); const [cornerStyle, setCornerStyle] = useState('square');
const [size, setSize] = useState(200); const [size, setSize] = useState(200);
useEffect(() => {
if (url) {
trackEvent('instant_qr_generated', {
url_length: url.length,
foreground: foregroundColor,
background: backgroundColor,
corner_style: cornerStyle,
size: size
});
}
}, [url, foregroundColor, backgroundColor, cornerStyle, size]);
const contrast = calculateContrast(foregroundColor, backgroundColor); const contrast = calculateContrast(foregroundColor, backgroundColor);
const hasGoodContrast = contrast >= 4.5; const hasGoodContrast = contrast >= 4.5;
@ -38,6 +52,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl); URL.revokeObjectURL(downloadUrl);
trackEvent('instant_qr_downloaded', { format: 'svg' });
} else { } else {
// Convert SVG to PNG using Canvas // Convert SVG to PNG using Canvas
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -65,6 +80,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl); URL.revokeObjectURL(downloadUrl);
trackEvent('instant_qr_downloaded', { format: 'png' });
} }
}); });
URL.revokeObjectURL(url); URL.revokeObjectURL(url);

View File

@ -130,7 +130,7 @@
"price": "€0", "price": "€0",
"period": "für immer", "period": "für immer",
"features": [ "features": [
"3 dynamische QR-Codes", "8 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes", "Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking", "Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen" "Standard QR-Design-Vorlagen"

View File

@ -137,7 +137,7 @@
"price": "€0", "price": "€0",
"period": "forever", "period": "forever",
"features": [ "features": [
"3 dynamic QR codes", "8 dynamic QR codes",
"Unlimited static QR codes", "Unlimited static QR codes",
"Basic scan tracking", "Basic scan tracking",
"Standard QR design templates", "Standard QR design templates",