import { useEffect, useRef, useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView, Animated, ActivityIndicator, Easing, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import Svg, { Path, Circle } from 'react-native-svg'; import type { ProtectionState } from '../../lib/protection'; import { apiFetch } from '../../lib/api'; import { useColors } from '../../lib/theme'; import { FormSheet } from '../FormSheet'; type Props = { visible: boolean; state: ProtectionState; onClose: () => void; onRequestDeactivation: () => void; onTalkToLyra: () => void; }; type StatsResponse = { current: number; weeklyAdded: number; monthlyAdded: number; history: { label: string; count: number }[]; submissions: { inVote: number; inReview: number }; mySubmissions: { active: number; inVote: number; inReview: number }; avgPerUser: number; avgApprovalWaitDays: number; }; // Brand colors const HERO_COLOR = '#f97316'; // orange-500 (counter accent) const SEG_ACTIVE = '#16a34a'; const SEG_VOTE = '#3b82f6'; const SEG_REVIEW = '#f59e0b'; export function ProtectionDetailsSheet({ visible, state, onClose, onRequestDeactivation, }: Props) { const { t, i18n } = useTranslation(); const colors = useColors(); const insets = useSafeAreaInsets(); const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US'; const [stats, setStats] = useState(null); const [loadingStats, setLoadingStats] = useState(false); useEffect(() => { if (!visible) return; let alive = true; setLoadingStats(true); apiFetch('/api/blocklist/stats') .then((res) => { if (alive) setStats(res); }) .catch(() => { /* silent */ }) .finally(() => { if (alive) setLoadingStats(false); }); return () => { alive = false; }; }, [visible]); const globalCount = stats?.current ?? state.blocklistCount; const weeklyAdded = stats?.weeklyAdded ?? 0; const monthlyAdded = stats?.monthlyAdded ?? 0; const myActive = stats?.mySubmissions?.active ?? 0; const myInVote = stats?.mySubmissions?.inVote ?? 0; const myInReview = stats?.mySubmissions?.inReview ?? 0; const avgPerUser = stats?.avgPerUser ?? 0; const avgWait = stats?.avgApprovalWaitDays ?? 0; return ( {loadingStats && !stats ? ( ) : null} {/* HERO – Globale geblockte Domains: Counter (slow, color) + 2 Delta-Badges */} {t('blocker.kpi_global_label')} {/* SUBMISSIONS – Half Donut mit center-number + center-legend */} {t('blocker.kpi_submissions_title')} {t('blocker.kpi_submissions_subtitle')} {/* Centered Legend */} {/* AVG KPIs – kleiner */} {/* FAQ-Banner: Heading-Row mit Help-Icon rechts (kein gestapeltes Layout) */} {t('blocker.faq_heading')} {[1, 2, 3, 4].map((n) => ( ))} {/* "Schutz deaktivieren" – outline button: TouchableOpacity=card, inner View=flex-row */} {t('blocker.more_info_title')} ); } // ─── Animated Counter ────────────────────────────────────────────────────── function AnimatedCounter({ value, locale, decimals = 0, durationMs = 1200, style, }: { value: number; locale: string; decimals?: number; durationMs?: number; style?: any; }) { const anim = useRef(new Animated.Value(0)).current; const [display, setDisplay] = useState(0); useEffect(() => { anim.setValue(0); const listener = anim.addListener(({ value: v }) => { setDisplay(v * value); }); Animated.timing(anim, { toValue: 1, duration: durationMs, easing: Easing.out(Easing.cubic), useNativeDriver: false, }).start(); return () => anim.removeListener(listener); }, [value, anim, durationMs]); const formatted = display.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); return {formatted}; } // ─── Delta Badge (e.g. "+25 diese Woche ↗") ──────────────────────────────── function DeltaBadge({ value, label, locale, }: { value: number; label: string; locale: string; }) { const formatted = `+${value.toLocaleString(locale)}`; return ( {formatted} {label} ); } // ─── KPI Card (small) ────────────────────────────────────────────────────── function KpiCard({ icon, label, value, locale, decimals = 0, suffix, }: { icon: any; label: string; value: number; locale: string; decimals?: number; suffix?: string; }) { const colors = useColors(); return ( {label} {suffix ? ( {suffix} ) : null} ); } // ─── Legend Item (compact, centered row) ─────────────────────────────────── function LegendItem({ color, label, value, }: { color: string; label: string; value: number; }) { const colors = useColors(); return ( {value} {label} ); } // ─── Half Donut (multi-segment) ──────────────────────────────────────────── function HalfDonut({ segments, centerValue, centerLabel, }: { segments: { value: number; color: string }[]; centerValue: number; centerLabel: string; }) { const colors = useColors(); const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0)); const W = 220; const H = 130; const cx = W / 2; const cy = H - 8; const r = 90; const stroke = 18; // Compute cumulative angles in [180, 360] let cumAngle = 180; const arcs = segments.map((seg) => { const startAngle = cumAngle; const endAngle = cumAngle + 180 * (seg.value / total); cumAngle = endAngle; return { ...seg, startAngle, endAngle }; }); const animProgress = useRef(new Animated.Value(0)).current; const [progress, setProgress] = useState(0); useEffect(() => { animProgress.setValue(0); const l = animProgress.addListener(({ value }) => setProgress(value)); Animated.timing(animProgress, { toValue: 1, duration: 1100, easing: Easing.out(Easing.cubic), useNativeDriver: false, }).start(); return () => animProgress.removeListener(l); }, [centerValue, animProgress]); return ( {/* Background track */} {arcs.map((a, i) => { const animatedEnd = a.startAngle + (a.endAngle - a.startAngle) * progress; if (animatedEnd <= a.startAngle + 0.5) return null; return ( ); })} {centerValue === 0 && ( )} {/* Center number — exactly centered horizontally + vertically inside semicircle */} {centerValue} {centerLabel} ); } function arcPath(cx: number, cy: number, r: number, startDeg: number, endDeg: number) { const start = polar(cx, cy, r, startDeg); const end = polar(cx, cy, r, endDeg); const largeArc = endDeg - startDeg > 180 ? 1 : 0; return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 1 ${end.x} ${end.y}`; } function polar(cx: number, cy: number, r: number, angleDeg: number) { const rad = (angleDeg * Math.PI) / 180; return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; } // ─── FAQ Item (chevron AT END of header row, on right) ───────────────────── function FaqItem({ question, answer }: { question: string; answer: string }) { const colors = useColors(); const [open, setOpen] = useState(false); const rotateAnim = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.timing(rotateAnim, { toValue: open ? 1 : 0, duration: 200, useNativeDriver: true, }).start(); }, [open, rotateAnim]); const rotate = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'], }); return ( setOpen((v) => !v)} activeOpacity={0.75} > {question} {open && ( {answer} )} ); }