hydration error

This commit is contained in:
Timo Knuth 2026-01-27 11:08:32 +01:00
parent 76a76258e8
commit be5db36b7f
7 changed files with 102 additions and 79 deletions

View File

@ -94,6 +94,13 @@ export default function CreatePage() {
} }
}, [contentType, frameOptions, frameType]); }, [contentType, frameOptions, frameType]);
// Force dynamic mode for COUPON and FEEDBACK types
useEffect(() => {
if (contentType === 'COUPON' || contentType === 'FEEDBACK') {
setIsDynamic(true);
}
}, [contentType]);
// Logo state // Logo state
const [logoUrl, setLogoUrl] = useState(''); const [logoUrl, setLogoUrl] = useState('');
const [logoSize, setLogoSize] = useState(24); const [logoSize, setLogoSize] = useState(24);
@ -712,14 +719,23 @@ export default function CreatePage() {
<span className="font-medium">Dynamic</span> <span className="font-medium">Dynamic</span>
<Badge variant="info" className="ml-2">Recommended</Badge> <Badge variant="info" className="ml-2">Recommended</Badge>
</label> </label>
<label className="flex items-center cursor-pointer"> <label className={cn(
"flex items-center",
(contentType === 'COUPON' || contentType === 'FEEDBACK')
? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
)}>
<input <input
type="radio" type="radio"
checked={!isDynamic} checked={!isDynamic}
onChange={() => setIsDynamic(false)} onChange={() => setIsDynamic(false)}
disabled={contentType === 'COUPON' || contentType === 'FEEDBACK'}
className="mr-2" className="mr-2"
/> />
<span className="font-medium">Static</span> <span className="font-medium">Static</span>
{(contentType === 'COUPON' || contentType === 'FEEDBACK') && (
<Tooltip text="Coupon and Feedback QR codes require dynamic features for tracking and analytics." />
)}
</label> </label>
</div> </div>
<p className="text-sm text-gray-600 mt-2"> <p className="text-sm text-gray-600 mt-2">
@ -727,6 +743,13 @@ export default function CreatePage() {
? '✅ Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.' ? '✅ Dynamic: Track scans, edit URL later, view analytics. QR contains tracking link.'
: '⚡ Static: Direct to content, no tracking, cannot edit. QR contains actual content.'} : '⚡ Static: Direct to content, no tracking, cannot edit. QR contains actual content.'}
</p> </p>
{(contentType === 'COUPON' || contentType === 'FEEDBACK') && (
<div className="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-900">
<strong>Note:</strong> {contentType === 'COUPON' ? 'Coupon' : 'Feedback'} QR codes must be Dynamic to track redemptions, collect feedback, and view detailed analytics.
</p>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>

View File

@ -26,9 +26,6 @@ export default function MarketingLayout({
setScrolled(window.scrollY > 20); setScrolled(window.scrollY > 20);
}; };
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll);
}, []); }, []);
@ -37,6 +34,7 @@ export default function MarketingLayout({
useEffect(() => { useEffect(() => {
setMobileMenuOpen(false); setMobileMenuOpen(false);
setToolsOpen(false); setToolsOpen(false);
setMobileToolsOpen(false);
}, [pathname]); }, [pathname]);
// Default to English for general marketing pages // Default to English for general marketing pages

View File

@ -81,7 +81,7 @@ export default function AuthorPage({ params }: { params: { slug: string } }) {
<div className="space-y-4"> <div className="space-y-4">
{posts.map(p => ( {posts.map(p => (
<Link key={p.slug} href={`/blog/${p.slug}`} className="block group p-6 rounded-xl border border-gray-200 bg-white hover:border-blue-200 hover:shadow-sm transition-all"> <Link key={p.slug} href={`/blog/${p.slug}`} className="block group p-6 rounded-xl border border-gray-200 bg-white hover:border-blue-200 hover:shadow-sm transition-all">
<div className="text-sm text-gray-400 mb-1">{new Date(p.datePublished || p.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div> <div className="text-sm text-gray-400 mb-1">{p.date}</div>
<h3 className="text-xl font-bold text-gray-900 group-hover:text-blue-700 transition-colors mb-2">{p.title}</h3> <h3 className="text-xl font-bold text-gray-900 group-hover:text-blue-700 transition-colors mb-2">{p.title}</h3>
<p className="text-gray-600">{p.description}</p> <p className="text-gray-600">{p.description}</p>
</Link> </Link>

View File

@ -55,7 +55,7 @@ export default function PillarPage({ params }: { params: { pillar: PillarKey } }
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{posts.map(p => ( {posts.map(p => (
<Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all"> <Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all">
<div className="text-xs text-gray-400 mb-2">{new Date(p.datePublished || p.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div> <div className="text-xs text-gray-400 mb-2">{p.date}</div>
<div className="text-lg font-bold text-gray-900 mb-2 group-hover:text-blue-700">{p.title}</div> <div className="text-lg font-bold text-gray-900 mb-2 group-hover:text-blue-700">{p.title}</div>
<div className="text-sm text-gray-600 line-clamp-2">{p.description}</div> <div className="text-sm text-gray-600 line-clamp-2">{p.description}</div>
</Link> </Link>

View File

@ -45,7 +45,7 @@ export default function LearnHubPage() {
<Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-2xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all"> <Link key={p.slug} href={`/blog/${p.slug}`} className="group block rounded-2xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md hover:border-blue-200 transition-all">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<div className="text-xs font-semibold px-2 py-1 rounded bg-gray-100 text-gray-600">{p.pillar?.toUpperCase() || 'GUIDE'}</div> <div className="text-xs font-semibold px-2 py-1 rounded bg-gray-100 text-gray-600">{p.pillar?.toUpperCase() || 'GUIDE'}</div>
<div className="text-xs text-gray-400">{new Date(p.datePublished || p.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div> <div className="text-xs text-gray-400">{p.date}</div>
</div> </div>
<div className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-700 line-clamp-2">{p.title}</div> <div className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-700 line-clamp-2">{p.title}</div>
<div className="text-gray-600 line-clamp-2">{p.description}</div> <div className="text-gray-600 line-clamp-2">{p.description}</div>

View File

@ -31,12 +31,12 @@ export default function CookieBanner() {
setShowBanner(false); setShowBanner(false);
}; };
if (!showBanner) return null;
return ( return (
<> <div suppressHydrationWarning>
{/* Cookie Banner - Bottom Left Corner */} {showBanner && (
<div className="fixed bottom-4 left-4 z-50 max-w-md animate-slide-in"> <>
{/* Cookie Banner - Bottom Left Corner */}
<div className="fixed bottom-4 left-4 z-50 max-w-md animate-slide-in">
<div className="bg-white rounded-lg shadow-2xl border border-gray-200 p-6"> <div className="bg-white rounded-lg shadow-2xl border border-gray-200 p-6">
<div className="flex items-start gap-3 mb-4"> <div className="flex items-start gap-3 mb-4">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@ -111,6 +111,8 @@ export default function CookieBanner() {
animation: slide-in 0.4s ease-out; animation: slide-in 0.4s ease-out;
} }
`}</style> `}</style>
</> </>
)}
</div>
); );
} }

View File

@ -1,67 +1,67 @@
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
type ClassValue = string | number | null | undefined | boolean | ClassValue[] | { [key: string]: any }; type ClassValue = string | number | null | undefined | boolean | ClassValue[] | { [key: string]: any };
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export function formatNumber(num: number): string { export function formatNumber(num: number): string {
if (num >= 1000000) { if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'; return (num / 1000000).toFixed(1) + 'M';
} }
if (num >= 1000) { if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'; return (num / 1000).toFixed(1) + 'K';
} }
return num.toString(); return num.toString();
} }
export function formatDate(date: Date | string): string { export function formatDate(date: Date | string): string {
const d = new Date(date); const d = new Date(date);
return d.toLocaleDateString('en-US', { return d.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
}); });
} }
export function formatDateTime(date: Date | string): string { export function formatDateTime(date: Date | string): string {
const d = new Date(date); const d = new Date(date);
return d.toLocaleDateString('en-US', { return d.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}); });
} }
export function calculateContrast(hex1: string, hex2: string): number { export function calculateContrast(hex1: string, hex2: string): number {
// Convert hex to RGB // Convert hex to RGB
const getRGB = (hex: string) => { const getRGB = (hex: string) => {
const r = parseInt(hex.slice(1, 3), 16); const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16); const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16); const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b]; return [r, g, b];
}; };
// Calculate relative luminance // Calculate relative luminance
const getLuminance = (rgb: number[]) => { const getLuminance = (rgb: number[]) => {
const [r, g, b] = rgb.map(c => { const [r, g, b] = rgb.map(c => {
c = c / 255; c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}); });
return 0.2126 * r + 0.7152 * g + 0.0722 * b; return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}; };
const rgb1 = getRGB(hex1); const rgb1 = getRGB(hex1);
const rgb2 = getRGB(hex2); const rgb2 = getRGB(hex2);
const lum1 = getLuminance(rgb1); const lum1 = getLuminance(rgb1);
const lum2 = getLuminance(rgb2); const lum2 = getLuminance(rgb2);
const brightest = Math.max(lum1, lum2); const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2); const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05); return (brightest + 0.05) / (darkest + 0.05);
} }