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 Link from "next/link";
|
||||||
import Logo from "@/components/Logo";
|
import Logo from "@/components/Logo";
|
||||||
import FlyerDownloadForm from "@/components/FlyerDownloadForm";
|
import FlyerDownloadForm from "@/components/FlyerDownloadForm";
|
||||||
|
import FlyerReportPreview from "@/components/FlyerReportPreview";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "MBO FamilyGuard – Familienschutz fürs Heimnetzwerk | MBO-Tech-IT",
|
title: "MBO FamilyGuard – Familienschutz fürs Heimnetzwerk | MBO-Tech-IT",
|
||||||
|
|
@ -180,6 +181,8 @@ export default function FamilyGuardPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FlyerReportPreview />
|
||||||
|
|
||||||
{/* Preis */}
|
{/* Preis */}
|
||||||
<div className="mb-16 rounded-2xl overflow-hidden border border-blue-500/30 bg-[#111925]">
|
<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">
|
<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