"use client"; import { useEffect, useMemo, useRef, useState, type ReactElement, type CSSProperties } from "react"; type DurationPoint = { dateISO: string; ms: number }; type SuccessStatsProps = { displayName: string; days: number; percent: number; totalDays: number; currentDurationMs: number | null; averageDurationMs: number | null; bestDurationMs: number | null; durationSeries: DurationPoint[]; measurementCount: number; showShortPasswordAlert: boolean; shortPasswordMessage: string; metricSuffix: string; currentSubtitle: string; chartTitle: string; isPasswordShort: boolean; passwordPolicyViolation: boolean; passwordLength: number; passwordHasUpper: boolean; passwordHasLower: boolean; passwordHasDigit: boolean; passwordHasSpecial: boolean; policyMinLength: number; }; const dateFormatter = new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", timeZone: "UTC", }); const disqualStampWrapperStyle: CSSProperties = { position: "absolute", top: "24px", left: "50%", transform: "translateX(-50%) rotate(-12deg)", pointerEvents: "none", zIndex: 50, }; const disqualStampStyle: CSSProperties = { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: "360px", padding: "18px 54px", border: "5px solid rgba(239,68,68,0.95)", background: "rgba(220,38,38,0.18)", color: "#fee2e2", fontSize: "1.25rem", fontWeight: 900, letterSpacing: "0.58em", textTransform: "uppercase", textShadow: "0 16px 38px rgba(220,38,38,0.6)", boxShadow: "0 20px 46px rgba(220,38,38,0.4)", whiteSpace: "nowrap", }; const disqualStampInnerStyle: CSSProperties = { position: "absolute", inset: "6px", border: "2px dashed rgba(248,113,113,0.7)", }; export default function SuccessStats({ displayName, days, percent, totalDays, currentDurationMs, averageDurationMs, bestDurationMs, durationSeries, measurementCount, showShortPasswordAlert, shortPasswordMessage, metricSuffix, currentSubtitle, chartTitle, isPasswordShort, passwordPolicyViolation, passwordLength, passwordHasUpper, passwordHasLower, passwordHasDigit, passwordHasSpecial, policyMinLength, }: SuccessStatsProps): ReactElement { const [dAnim, setDAnim] = useState(0); const [pAnim, setPAnim] = useState(0); const cardRef = useRef(null); useEffect(() => { animateTo(days, setDAnim, 800); animateTo(percent, setPAnim, 900); cardRef.current?.animate( [ { boxShadow: "0 10px 32px rgba(0,0,0,.45)" }, { boxShadow: "0 16px 40px rgba(89,240,168,.18)" }, { boxShadow: "0 10px 32px rgba(0,0,0,.45)" }, ], { duration: 800, easing: "ease-in-out" } ); }, [days, percent]); const hasMeasurements = measurementCount > 0; const hasAverage = averageDurationMs !== null; const currentTone = useMemo(() => { if (!hasAverage || currentDurationMs === null) return "neutral" as const; if (currentDurationMs > averageDurationMs!) return "slower" as const; if (currentDurationMs < averageDurationMs!) return "faster" as const; return "neutral" as const; }, [hasAverage, currentDurationMs, averageDurationMs]); const chartStats = useMemo(() => { if (!hasMeasurements) return null; const values = durationSeries.map((point) => point.ms); const max = Math.max(...values); const min = Math.min(...values); return { max, min }; }, [durationSeries, hasMeasurements]); const passwordChecks = useMemo(() => { const lengthOk = passwordLength >= policyMinLength; const upperOk = passwordHasUpper; const lowerOk = passwordHasLower; const digitOk = passwordHasDigit; const specialOk = passwordHasSpecial; return { lengthOk, upperOk, lowerOk, digitOk, specialOk, allOk: lengthOk && upperOk && lowerOk && digitOk && specialOk, }; }, [passwordLength, passwordHasUpper, passwordHasLower, passwordHasDigit, passwordHasSpecial, policyMinLength]); const stampNode = passwordPolicyViolation ? ( ) : null; const headerNode = (

{passwordPolicyViolation ? "Login erfolgreich, aber das Passwort ist nicht gut!" : "Login erfolgreich!"}

{isPasswordShort ? "Dein Passwort ist zu kurz" : "Gut gemacht"},{" "} {displayName}.

); const alertNode = showShortPasswordAlert ? (
{shortPasswordMessage}
) : null; const passwordTile = (
Passwort-Check

{passwordChecks.allOk ? "Starke Passwort-Anforderungen erfüllt." : "Bitte verbessere dein Passwort, um alle Kriterien zu erfüllen."}

); if (passwordPolicyViolation) { return (
{stampNode} {headerNode} {alertNode}
{passwordTile}
Zurück zum Login
); } return (
{stampNode} {headerNode} {alertNode}
Letzte {totalDays} Tage
{dAnim}
{dAnim === 1 ? "Tag mit Erfolg" : "Tage mit Erfolg"}
Rating
{pAnim}%
Durchhaltequote
Aktueller Login
{renderDuration(currentDurationMs, metricSuffix)}
{currentSubtitle}
{bestDurationMs !== null && (
Deine Bestzeit: {renderDuration(bestDurationMs, metricSuffix)}
)}
{passwordTile}

{chartTitle}

Werte in Sekunden{metricSuffix ? ` ${metricSuffix}` : ""},{" "} {measurementCount === 1 ? "1 Messung" : `${measurementCount} Messungen`}
{hasMeasurements && chartStats ? ( ) : (

Noch keine Messwerte vorhanden. Melde dich erneut an, um Daten zu sammeln.

)}
Zurück zum Login
); } function animateTo(target: number, set: (v: number) => void, duration = 800): void { const start = performance.now(); const from = 0; const to = Math.max(0, Math.round(target)); const ease = (t: number): number => 1 - Math.pow(1 - t, 3); function frame(now: number): void { const p = Math.min(1, (now - start) / duration); const val = Math.round(from + (to - from) * ease(p)); set(val); if (p < 1) requestAnimationFrame(frame); } requestAnimationFrame(frame); } function formatDuration(ms: number | null): string { if (ms === null || ms === undefined) return "–"; const seconds = ms / 1000; if (seconds < 60) { return `${seconds.toFixed(seconds < 10 ? 2 : 1)} s`; } const minutes = Math.floor(seconds / 60); const rest = seconds - minutes * 60; const restStr = rest >= 10 ? rest.toFixed(0) : rest.toFixed(1); return `${minutes} min ${restStr} s`; } function renderDuration(ms: number | null, suffix = ""): ReactElement { if (ms === null || ms === undefined) return <>–; return ( <> {formatDuration(ms)} {suffix && {suffix}} ); } function SecurityRow({ ok, label }: { ok: boolean; label: string }): ReactElement { return (
{ok ? "✅" : "🚨"} {label}
); } function DurationChart({ series, minMs, maxMs, valueSuffix, }: { series: DurationPoint[]; minMs: number; maxMs: number; valueSuffix: string; }): ReactElement { const padding = 8; const width = 100; const height = 80; const usableWidth = width - padding * 2; const usableHeight = height - padding * 2; const range = Math.max(1, maxMs - minMs); const points = series.map((entry, index) => { const x = series.length === 1 ? width / 2 : padding + (usableWidth * index) / (series.length - 1); const y = height - (padding + ((entry.ms - minMs) / range) * usableHeight); return { ...entry, x, y }; }); const polyline = points.length === 1 ? `${points[0].x},${points[0].y}` : points.map((p) => `${p.x},${p.y}`).join(" "); return (
{points.length > 1 && ( `${p.x},${p.y}`) .join(" ")} ${points[points.length - 1].x},${height - padding} ${points[0].x},${height - padding}`} fill="url(#login-time-gradient)" opacity={0.35} /> )} {points.map((p, idx) => ( {`${dateFormatter.format(new Date(p.dateISO))} · ${(p.ms / 1000).toFixed(2)} s${ valueSuffix ? ` ${valueSuffix}` : "" }`} ))}
{(minMs / 1000).toFixed(2)} s{valueSuffix ? ` ${valueSuffix}` : ""} Max {(maxMs / 1000).toFixed(2)} s{valueSuffix ? ` ${valueSuffix}` : ""}
{points.map((p, idx) => ( {dateFormatter.format(new Date(p.dateISO))} ))}
); }