feat: add example report preview with stats and chart to FamilyGuard page
Zeigt beispielhaft, wie die monatliche FamilyGuard-Report-E-Mail aussieht (Stat-Kacheln + Balkendiagramm der letzten 6 Monate), direkt nach dem Report-Angebot platziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhY1QhDXGWfyhrxaJvfpNt
This commit is contained in:
parent
a26cc3f8f2
commit
0183430725
|
|
@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||
import Link from "next/link";
|
||||
import Logo from "@/components/Logo";
|
||||
import FlyerDownloadForm from "@/components/FlyerDownloadForm";
|
||||
import FlyerReportPreview from "@/components/FlyerReportPreview";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "MBO FamilyGuard – Familienschutz fürs Heimnetzwerk | MBO-Tech-IT",
|
||||
|
|
@ -180,6 +181,8 @@ export default function FamilyGuardPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<FlyerReportPreview />
|
||||
|
||||
{/* Preis */}
|
||||
<div className="mb-16 rounded-2xl overflow-hidden border border-blue-500/30 bg-[#111925]">
|
||||
<div className="p-8 flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue