fix(blog): optimize component share logic, typography, and modal layouts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-22 11:41:28 +01:00
parent 75c61f1436
commit b15c8408ff
103 changed files with 4366 additions and 2293 deletions

View File

@@ -0,0 +1,137 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
interface BoldNumberProps {
/** The number to display, e.g. "53%" or "2.5M€" or "-20%" */
value: string;
/** Short description of what this number means */
label: string;
/** Source attribution */
source?: string;
/** Source URL */
sourceUrl?: string;
className?: string;
}
/**
* Premium hero number component — full-width, dark gradient, animated count-up.
* Designed for shareable key statistics that stand out in blog posts.
*/
export const BoldNumber: React.FC<BoldNumberProps> = ({
value,
label,
source,
sourceUrl,
className = '',
}) => {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const [displayValue, setDisplayValue] = useState('');
// Extract numeric part for animation
const numericMatch = value.match(/^([+-]?)(\d+(?:[.,]\d+)?)(.*)/);
const prefix = numericMatch?.[1] ?? '';
const numStr = numericMatch?.[2] ?? '';
const suffix = numericMatch?.[3] ?? value;
const targetNum = parseFloat(numStr.replace(',', '.')) || 0;
const hasDecimals = numStr.includes('.') || numStr.includes(',');
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.3 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!isVisible || !numStr) {
setDisplayValue(value);
return;
}
const duration = 1200;
const steps = 40;
const stepTime = duration / steps;
let step = 0;
const timer = setInterval(() => {
step++;
const progress = Math.min(step / steps, 1);
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3);
const current = targetNum * eased;
const formatted = hasDecimals ? current.toFixed(1) : Math.round(current).toString();
setDisplayValue(`${prefix}${formatted}${suffix}`);
if (step >= steps) {
clearInterval(timer);
setDisplayValue(value);
}
}, stepTime);
return () => clearInterval(timer);
}, [isVisible, value, prefix, suffix, targetNum, hasDecimals, numStr]);
const handleShare = async () => {
const shareText = `${value}${label}${source ? ` (${source})` : ''}`;
try {
if (navigator.share) {
await navigator.share({ text: shareText });
} else {
await navigator.clipboard.writeText(shareText);
}
} catch { /* user cancelled */ }
};
return (
<div
ref={ref}
className={`not-prose relative overflow-hidden rounded-2xl my-16 border border-slate-100 bg-slate-50/50 p-10 md:p-14 text-center ${className}`}
>
<div className="relative z-10">
<span className="block text-6xl md:text-8xl font-black tracking-tighter tabular-nums leading-none text-slate-900 pb-2">
{displayValue || value}
</span>
<span className="block mt-4 text-base md:text-lg font-medium text-slate-500 uppercase tracking-widest max-w-lg mx-auto">
{label}
</span>
{source && (
<span className="block mt-4 text-xs font-semibold text-slate-400">
{sourceUrl ? (
<a href={sourceUrl} target="_blank" rel="noopener noreferrer" className="hover:text-blue-600 transition-colors">
Quelle: {source}
</a>
) : (
`Quelle: ${source}`
)}
</span>
)}
</div>
{/* Share button - subtle now */}
<button
onClick={handleShare}
className="absolute top-4 right-4 z-20 p-2 rounded-lg text-slate-300 hover:text-blue-600 hover:bg-blue-50 transition-all cursor-pointer"
title="Teilen"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
</button>
</div>
);
};