Alle <Pressable style={({pressed}) => ({...})}> ersetzt — style-Funktion
droppt auf Android (New Arch) intermittierend width/height, führt zu 0×0
unsichtbaren Elementen. TouchableOpacity mit activeOpacity ist stabil.
Außerdem übrige Pressables (plain style) aus components/ und app/
migriert sowie zwei überschüssige </View>-Tags in chat.tsx + RoomCard.tsx
entfernt die TS-Fehler verursacht haben.
64 Dateien, typecheck sauber.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
161 lines
4.4 KiB
TypeScript
161 lines
4.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { TouchableOpacity, Text, View } from 'react-native';
|
|
import Svg, { Rect, Text as SvgText } from 'react-native-svg';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useColors } from '../../lib/theme';
|
|
import type { DailyStat } from '../../hooks/useMailStatus';
|
|
|
|
type Props = {
|
|
dailyStats: DailyStat[];
|
|
totalBlocked: number;
|
|
};
|
|
|
|
const CHART_HEIGHT = 72;
|
|
const BAR_RADIUS = 4;
|
|
const LABEL_HEIGHT = 16;
|
|
const SVG_HEIGHT = CHART_HEIGHT + LABEL_HEIGHT;
|
|
|
|
export function MailWeeklyChart({ dailyStats, totalBlocked }: Props) {
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const [activeIdx, setActiveIdx] = useState<number | null>(null);
|
|
|
|
const chartMax = Math.max(...dailyStats.map((d) => d.count), 1);
|
|
|
|
const weekTotal = dailyStats.reduce((s, d) => s + d.count, 0);
|
|
|
|
return (
|
|
<View
|
|
style={{
|
|
backgroundColor: colors.surface,
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
paddingHorizontal: 16,
|
|
paddingTop: 14,
|
|
paddingBottom: 14,
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 12,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
{t('mail.chart_title')}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: colors.error,
|
|
}}
|
|
>
|
|
{t('mail.chart_week_total', { count: weekTotal })}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Tooltip */}
|
|
{activeIdx !== null && dailyStats[activeIdx] !== undefined && (
|
|
<View
|
|
style={{
|
|
alignSelf: 'center',
|
|
backgroundColor: colors.surfaceElevated,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
marginBottom: 8,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: colors.text,
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
{dailyStats[activeIdx].label}: {dailyStats[activeIdx].count}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* SVG Bar Chart */}
|
|
<View style={{ width: '100%' }}>
|
|
<Svg width="100%" height={SVG_HEIGHT} viewBox={`0 0 ${7 * 40} ${SVG_HEIGHT}`} preserveAspectRatio="none">
|
|
{dailyStats.map((day, i) => {
|
|
const barH = day.count > 0
|
|
? Math.max(6, Math.round((day.count / chartMax) * CHART_HEIGHT))
|
|
: 4;
|
|
const x = i * 40 + 4;
|
|
const barW = 32;
|
|
const y = CHART_HEIGHT - barH;
|
|
const isActive = activeIdx === i;
|
|
const fill = day.count > 0
|
|
? isActive ? '#b91c1c' : '#ef4444'
|
|
: colors.border;
|
|
|
|
return (
|
|
<Rect
|
|
key={day.date}
|
|
x={x}
|
|
y={y}
|
|
width={barW}
|
|
height={barH}
|
|
rx={BAR_RADIUS}
|
|
ry={BAR_RADIUS}
|
|
fill={fill}
|
|
/>
|
|
);
|
|
})}
|
|
{dailyStats.map((day, i) => (
|
|
<SvgText
|
|
key={`label-${day.date}`}
|
|
x={i * 40 + 20}
|
|
y={SVG_HEIGHT - 2}
|
|
textAnchor="middle"
|
|
fontSize={10}
|
|
fill={colors.textMuted}
|
|
fontFamily="Nunito_400Regular"
|
|
>
|
|
{day.label}
|
|
</SvgText>
|
|
))}
|
|
</Svg>
|
|
|
|
{/* Invisible tap targets per bar */}
|
|
<View
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
flexDirection: 'row',
|
|
}}
|
|
>
|
|
{dailyStats.map((day, i) => (
|
|
<TouchableOpacity
|
|
key={`tap-${day.date}`}
|
|
style={{ flex: 1, height: '100%' }}
|
|
onPress={() => setActiveIdx((prev) => (prev === i ? null : i))}
|
|
activeOpacity={0.7}
|
|
accessibilityLabel={`${day.label}: ${day.count}`}
|
|
/>
|
|
))}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|