Merge branch 'dynamisch' into master (favoring dynamisch changes)

This commit is contained in:
Timo
2026-01-22 19:37:15 +01:00
59 changed files with 11848 additions and 4922 deletions

View File

@@ -1,102 +1,104 @@
'use client';
import { useEffect, useState, useRef } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import posthog from 'posthog-js';
export function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
// Track page views
useEffect(() => {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && pathname) {
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 = useRef(false);
// Initialize PostHog once
useEffect(() => {
// Prevent double initialization in React Strict Mode
if (initializationAttempted.current) return;
initializationAttempted.current = true;
const cookieConsent = localStorage.getItem('cookieConsent');
// Check if we should initialize based on consent
// If not accepted yet, we don't init. CookieBanner deals with setting 'accepted' and reloading or calling init.
// Ideally we should listen to consent changes, but for now matching previous behavior.
if (cookieConsent === 'accepted') {
const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const apiHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
if (!apiKey) {
console.warn('PostHog API key not configured');
return;
}
if (!(posthog as any)._loaded) {
posthog.init(apiKey, {
api_host: apiHost || 'https://us.i.posthog.com',
person_profiles: 'identified_only',
capture_pageview: false, // We handle this manually
capture_pageleave: true,
autocapture: true,
respect_dnt: true,
opt_out_capturing_by_default: false,
});
if (process.env.NODE_ENV === 'development') {
posthog.debug();
}
}
}
}, []);
return <>{children}</>;
}
/**
* Helper function to identify user after login
*/
export function identifyUser(userId: string, traits?: Record<string, any>) {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && (posthog as any)._loaded) {
posthog.identify(userId, traits);
}
}
/**
* Helper function to track custom events
*/
export function trackEvent(eventName: string, properties?: Record<string, any>) {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && (posthog as any)._loaded) {
posthog.capture(eventName, properties);
}
}
/**
* Helper function to reset user on logout
*/
export function resetUser() {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && (posthog as any)._loaded) {
posthog.reset();
}
}
'use client';
import { useEffect, useState, useRef } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import posthog from 'posthog-js';
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [isInitialized, setIsInitialized] = useState(false);
const initializationAttempted = useRef(false);
// Initialize PostHog once
useEffect(() => {
// Prevent double initialization in React Strict Mode
if (initializationAttempted.current) return;
initializationAttempted.current = true;
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted') {
const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const apiHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
if (!apiKey) {
console.warn('PostHog API key not configured');
return;
}
// Check if already initialized (using _loaded property)
if (!(posthog as any)._loaded) {
posthog.init(apiKey, {
api_host: apiHost || 'https://us.i.posthog.com',
person_profiles: 'identified_only',
capture_pageview: false, // Manual pageview tracking
capture_pageleave: true,
autocapture: true,
respect_dnt: true,
opt_out_capturing_by_default: false,
});
// Enable debug mode in development
if (process.env.NODE_ENV === 'development') {
posthog.debug();
}
// Set initialized immediately after init
setIsInitialized(true);
} else {
setIsInitialized(true); // Already loaded
}
}
// NO cleanup function - PostHog should persist across page navigation
}, []);
// Track page views ONLY after PostHog is initialized
useEffect(() => {
const cookieConsent = localStorage.getItem('cookieConsent');
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}</>;
}
/**
* Helper function to identify user after login
*/
export function identifyUser(userId: string, traits?: Record<string, any>) {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && (posthog as any)._loaded) {
posthog.identify(userId, traits);
}
}
/**
* Helper function to track custom events
*/
export function trackEvent(eventName: string, properties?: Record<string, any>) {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && (posthog as any)._loaded) {
posthog.capture(eventName, properties);
}
}
/**
* Helper function to reset user on logout
*/
export function resetUser() {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && (posthog as any)._loaded) {
posthog.reset();
}
}

View File

@@ -1,30 +1,23 @@
import React from 'react';
interface SeoJsonLdProps {
data: object | object[];
}
export default function SeoJsonLd({ data }: SeoJsonLdProps) {
const jsonLdArray = Array.isArray(data) ? data : [data];
return (
<>
{jsonLdArray.map((item, index) => {
// Only add @context if it doesn't already exist in the item
const schema = (item as any)['@context']
? item
: { '@context': 'https://schema.org', ...item };
return (
<script
key={index}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(schema, null, 0),
}}
/>
);
})}
</>
);
}
import React from 'react';
interface SeoJsonLdProps {
data: object | object[];
}
export default function SeoJsonLd({ data }: SeoJsonLdProps) {
const jsonLdArray = Array.isArray(data) ? data : [data];
return (
<>
{jsonLdArray.map((item, index) => (
<script
key={index}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(item, null, 0),
}}
/>
))}
</>
);
}

View File

@@ -168,6 +168,7 @@ END:VCARD`;
</button>
}
>
<DropdownItem onClick={() => window.location.href = `/qr/${qr.id}`}>View Details</DropdownItem>
<DropdownItem onClick={() => downloadQR('png')}>Download PNG</DropdownItem>
<DropdownItem onClick={() => downloadQR('svg')}>Download SVG</DropdownItem>
{qr.type === 'DYNAMIC' && (
@@ -246,6 +247,15 @@ END:VCARD`;
</p>
</div>
)}
{/* Feedback Button - only for FEEDBACK type */}
{qr.contentType === 'FEEDBACK' && (
<button
onClick={() => window.location.href = `/qr/${qr.id}/feedback`}
className="w-full mt-3 py-2 px-3 bg-amber-50 hover:bg-amber-100 text-amber-700 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors"
>
View Customer Feedback
</button>
)}
</div>
</CardContent>
</Card>

View File

@@ -89,4 +89,4 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -82,4 +82,4 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -222,9 +222,18 @@ export function FreeToolsGrid() {
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900 mb-4">
More Free QR Code Tools
</h2>
<div className="flex flex-col md:flex-row items-center justify-center gap-3 mb-4">
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900">
More Free QR Code Tools
</h2>
<div className="bg-gradient-to-r from-emerald-500 to-green-500 text-white px-3 py-1 rounded-full text-xs md:text-sm font-semibold shadow-lg shadow-emerald-500/20 flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
</span>
Free Forever
</div>
</div>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Create specialized QR codes for every need. Completely free and no signup required.
</p>

View File

@@ -6,19 +6,77 @@ import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card } from '@/components/ui/Card';
import { motion } from 'framer-motion';
import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight } from 'lucide-react';
import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight, FileText, Ticket, Smartphone, Star } from 'lucide-react';
import { useState, useEffect } from 'react';
// Sub-component for the flipping effect
const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
const [isFlipped, setIsFlipped] = useState(false);
useEffect(() => {
// Initial delay
const initialTimeout = setTimeout(() => {
setIsFlipped(true); // First flip
// Setup interval for subsequent flips
const interval = setInterval(() => {
setIsFlipped(prev => !prev);
}, 8000); // Toggle every 8 seconds to prevent overlap (4 cards * 2s gap)
return () => clearInterval(interval);
}, delay * 1000);
return () => clearTimeout(initialTimeout);
}, [delay]);
return (
<div className="relative h-32 w-full perspective-[1000px] group cursor-pointer">
<motion.div
animate={{ rotateY: isFlipped ? 180 : 0 }}
transition={{ duration: 0.6, type: "spring", stiffness: 260, damping: 20 }}
className="relative w-full h-full preserve-3d"
style={{ transformStyle: 'preserve-3d' }}
>
{/* Front Face */}
<div
className="absolute inset-0 backface-hidden"
style={{ backfaceVisibility: 'hidden', WebkitBackfaceVisibility: 'hidden' }}
>
<Card className="w-full h-full backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
<div className={`w-10 h-10 mb-3 rounded-xl ${front.color} flex items-center justify-center`}>
<front.icon className="w-5 h-5" />
</div>
<p className="font-semibold text-gray-800 text-sm">{front.title}</p>
</Card>
</div>
{/* Back Face */}
<div
className="absolute inset-0 backface-hidden"
style={{
backfaceVisibility: 'hidden',
WebkitBackfaceVisibility: 'hidden',
transform: 'rotateY(180deg)'
}}
>
<Card className="w-full h-full backdrop-blur-xl bg-white/80 border-white/60 shadow-xl shadow-blue-200/50 p-4 flex flex-col items-center justify-center hover:scale-105 transition-all duration-300">
<div className={`w-10 h-10 mb-3 rounded-xl ${back.color} flex items-center justify-center`}>
<back.icon className="w-5 h-5" />
</div>
<p className="font-semibold text-gray-900 text-sm">{back.title}</p>
</Card>
</div>
</motion.div>
</div>
);
};
interface HeroProps {
t: any; // i18n translation function
}
export const Hero: React.FC<HeroProps> = ({ t }) => {
const templateCards = [
{ title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
{ title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
{ title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
{ title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
];
const containerjs = {
hidden: { opacity: 0 },
@@ -66,9 +124,9 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
transition={{ duration: 0.5 }}
className="space-y-6"
>
<h2 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
{t.hero.title}
</h2>
</h1>
<p className="text-xl text-gray-600 leading-relaxed max-w-2xl">
{t.hero.subtitle}
@@ -113,37 +171,34 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
{/* Right Preview Widget */}
<div className="relative">
<motion.div
variants={containerjs}
initial="hidden"
animate="show"
className="grid grid-cols-2 gap-4"
>
{templateCards.map((card, index) => (
<motion.div key={index} variants={itemjs}>
<Card className={`backdrop-blur-xl bg-white/70 border-white/50 shadow-xl shadow-gray-200/50 p-6 text-center hover:scale-105 transition-all duration-300 group cursor-pointer`}>
<div className={`w-12 h-12 mx-auto mb-4 rounded-xl ${card.color} flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
<card.icon className="w-6 h-6" />
</div>
<p className="font-semibold text-gray-800 group-hover:text-gray-900">{card.title}</p>
</Card>
</motion.div>
))}
</motion.div>
{/* Floating Badge */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8 }}
className="absolute -top-4 -right-4 bg-gradient-to-r from-success-500 to-emerald-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg shadow-success-500/30 flex items-center gap-2"
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-white"></span>
</span>
{t.hero.engagement_badge}
</motion.div>
<div className="relative perspective-[1000px]">
<div className="grid grid-cols-2 gap-4">
{[
{
front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText },
delay: 3 // Starts at 3s
},
{
front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket },
delay: 5 // +2s
},
{
front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone },
delay: 7 // +2s
},
{
front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star },
delay: 9 // +2s
},
].map((card, index) => (
<FlippingCard key={index} {...card} />
))}
</div>
</div>
</div>
</div>
</div>
@@ -152,4 +207,4 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-b from-transparent to-gray-50 pointer-events-none" />
</section >
);
};
};

View File

@@ -8,7 +8,6 @@ import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { calculateContrast } from '@/lib/utils';
import AdBanner from '@/components/ads/AdBanner';
interface InstantGeneratorProps {
t: any; // i18n translation function
@@ -280,4 +279,4 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -138,4 +138,4 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -95,4 +95,4 @@ export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -32,4 +32,4 @@ export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -67,4 +67,4 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
</div>
</section>
);
};
};