Paar fehler
This commit is contained in:
parent
e44dc1c6bb
commit
59131a54f0
|
|
@ -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*
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
@ -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}</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue