147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
|
||
const monthlyData = [
|
||
{ month: "Okt", value: 189 },
|
||
{ month: "Nov", value: 203 },
|
||
{ month: "Dez", value: 231 },
|
||
{ month: "Jan", value: 198 },
|
||
{ month: "Feb", value: 215 },
|
||
{ month: "Mär", value: 247 },
|
||
];
|
||
|
||
const maxValue = Math.max(...monthlyData.map((d) => d.value));
|
||
|
||
const stats = [
|
||
{ value: 247, suffix: "", label: "Blockierte Gefahren (Mär)", accent: "orange" as const },
|
||
{ value: 6, suffix: "", label: "Geschützte Geräte", accent: "blue" as const },
|
||
{ value: 100, suffix: "%", label: "Automatisch aktuell", accent: "orange" as const },
|
||
];
|
||
|
||
function Counter({
|
||
value,
|
||
suffix,
|
||
accent,
|
||
}: {
|
||
value: number;
|
||
suffix: string;
|
||
accent: "orange" | "blue";
|
||
}) {
|
||
const [count, setCount] = useState(0);
|
||
const ref = useRef<HTMLSpanElement>(null);
|
||
const started = useRef(false);
|
||
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el) return;
|
||
|
||
const observer = new IntersectionObserver(
|
||
([entry]) => {
|
||
if (entry.isIntersecting && !started.current) {
|
||
started.current = true;
|
||
const duration = 1200;
|
||
const start = performance.now();
|
||
|
||
const tick = (now: number) => {
|
||
const progress = Math.min((now - start) / duration, 1);
|
||
const eased = 1 - Math.pow(1 - progress, 3);
|
||
setCount(Math.floor(eased * value));
|
||
if (progress < 1) requestAnimationFrame(tick);
|
||
};
|
||
|
||
requestAnimationFrame(tick);
|
||
observer.disconnect();
|
||
}
|
||
},
|
||
{ threshold: 0.3 }
|
||
);
|
||
|
||
observer.observe(el);
|
||
return () => observer.disconnect();
|
||
}, [value]);
|
||
|
||
const color = accent === "orange" ? "text-orange-400" : "text-blue-400";
|
||
|
||
return (
|
||
<span ref={ref} className={`text-3xl sm:text-4xl font-black ${color}`}>
|
||
{count}
|
||
{suffix}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export default function FlyerReportPreview() {
|
||
return (
|
||
<div className="mb-16 p-6 sm:p-8 rounded-2xl bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-800">
|
||
<div className="text-center mb-8">
|
||
<span className="text-orange-400 font-mono text-xs font-bold tracking-[0.25em] uppercase">
|
||
Beispiel-Report
|
||
</span>
|
||
<h3 className="text-2xl font-black text-slate-900 dark:text-white mt-1 mb-2">
|
||
So sieht Ihre monatliche Übersicht aus
|
||
</h3>
|
||
<p className="text-slate-600 dark:text-slate-400 text-sm max-w-xl mx-auto">
|
||
Beispielhafte Darstellung des MBO FamilyGuard Reports – Ihre echte
|
||
monatliche E-Mail zeigt die tatsächlichen Werte für Ihr Zuhause.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Stat-Kacheln */}
|
||
<div className="grid grid-cols-3 gap-3 sm:gap-4 mb-10">
|
||
{stats.map((s) => (
|
||
<div
|
||
key={s.label}
|
||
className="text-center p-4 rounded-xl bg-slate-50 dark:bg-[#111925] border border-slate-200 dark:border-gray-800"
|
||
>
|
||
<Counter value={s.value} suffix={s.suffix} accent={s.accent} />
|
||
<p className="text-slate-600 dark:text-slate-400 text-[11px] sm:text-xs mt-2 leading-snug">
|
||
{s.label}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Balkendiagramm */}
|
||
<div>
|
||
<p className="text-slate-700 dark:text-slate-300 text-sm font-semibold mb-4 text-center">
|
||
Blockierte Gefahren – letzte 6 Monate
|
||
</p>
|
||
<div className="flex items-end justify-between gap-3 h-28 px-2">
|
||
{monthlyData.map((d, i) => {
|
||
const isCurrent = i === monthlyData.length - 1;
|
||
const heightPct = (d.value / maxValue) * 100;
|
||
return (
|
||
<div key={d.month} className="flex-1 h-full flex flex-col justify-end items-center">
|
||
<span
|
||
className={`text-[11px] font-semibold mb-1 ${
|
||
isCurrent ? "text-orange-500 dark:text-orange-400" : "text-slate-400 dark:text-slate-500"
|
||
}`}
|
||
>
|
||
{d.value}
|
||
</span>
|
||
<div
|
||
className={`w-full max-w-[32px] rounded-t-md ${
|
||
isCurrent ? "bg-orange-500" : "bg-orange-500/25"
|
||
}`}
|
||
style={{ height: `${heightPct}%` }}
|
||
/>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
<div className="flex justify-between gap-3 px-2 mt-2">
|
||
{monthlyData.map((d) => (
|
||
<span
|
||
key={d.month}
|
||
className="flex-1 text-center text-[11px] text-slate-500 dark:text-slate-500"
|
||
>
|
||
{d.month}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|