MBO-Tech-IT-Webseite/components/FlyerReportPreview.tsx

185 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 topThreats = [
{ label: "Werbung & Tracker", value: 112 },
{ label: "Erwachseneninhalte", value: 54 },
{ label: "Phishing-Versuche", value: 38 },
{ label: "Schadsoftware-Domains", value: 24 },
{ label: "Social-Media-Tracker", value: 19 },
];
const maxThreatValue = Math.max(...topThreats.map((t) => t.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>
{/* Top 5 Gefahrenkategorien */}
<div className="mt-10">
<p className="text-slate-700 dark:text-slate-300 text-sm font-semibold mb-4 text-center">
Top 5 blockierte Gefahren (Mär)
</p>
<div className="space-y-3 max-w-lg mx-auto">
{topThreats.map((t, i) => (
<div key={t.label} className="flex items-center gap-3">
<span className="w-5 flex-shrink-0 text-xs font-bold text-slate-400 dark:text-slate-500">
{i + 1}
</span>
<span className="w-36 sm:w-40 flex-shrink-0 text-xs text-slate-600 dark:text-slate-400 truncate">
{t.label}
</span>
<div className="flex-1 h-2 rounded-full bg-slate-100 dark:bg-[#111925] overflow-hidden">
<div
className="h-full rounded-full bg-orange-500"
style={{ width: `${(t.value / maxThreatValue) * 100}%` }}
/>
</div>
<span className="w-8 flex-shrink-0 text-xs font-semibold text-slate-700 dark:text-slate-300 text-right">
{t.value}
</span>
</div>
))}
</div>
</div>
</div>
);
}