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]);
// Force dynamic mode for COUPON and FEEDBACK types
useEffect(() => {
if (contentType === 'COUPON' || contentType === 'FEEDBACK') {
setIsDynamic(true);
}
}, [contentType]);
// Logo state
const [logoUrl, setLogoUrl] = useState('');
const [logoSize, setLogoSize] = useState(24);
@ -712,14 +719,23 @@ export default function CreatePage() {
<span className="font-medium">Dynamic</span>
<Badge variant="info" className="ml-2">Recommended</Badge>
</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
type="radio"
checked={!isDynamic}
onChange={() => setIsDynamic(false)}
disabled={contentType === 'COUPON' || contentType === 'FEEDBACK'}
className="mr-2"
/>
<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>
</div>
<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.'
: '⚡ Static: Direct to content, no tracking, cannot edit. QR contains actual content.'}
</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>
</Card>

View File

@ -26,9 +26,6 @@ export default function MarketingLayout({
setScrolled(window.scrollY > 20);
};
// Check immediately on mount
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
@ -37,6 +34,7 @@ export default function MarketingLayout({
useEffect(() => {
setMobileMenuOpen(false);
setToolsOpen(false);
setMobileToolsOpen(false);
}, [pathname]);
// 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">
{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">
<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>
<p className="text-gray-600">{p.description}</p>
</Link>

View File

@ -55,7 +55,7 @@ export default function PillarPage({ params }: { params: { pillar: PillarKey } }
<div className="grid md:grid-cols-2 gap-6">
{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">
<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-sm text-gray-600 line-clamp-2">{p.description}</div>
</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">
<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 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 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>

View File

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

View File

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