chahinebrini 2c1eecd1f7 feat(native): geräte-spezifische PNG-Icons (iphone/android/macbook/computer)
deviceImage()-Helper mappt Plattform→assets/devices/*.png; ersetzt Ionicons
in Geräte-Rows, MagicSheet und Detail-Sheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:40:25 +02:00

264 lines
8.6 KiB
TypeScript

import { useMemo } from 'react';
import { Image, type ImageSourcePropType, Text, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
import { FormSheet } from '../FormSheet';
import { HalfDonut } from '../common/HalfDonut';
import { useProtectionCoverage } from '../../hooks/useProfileData';
const PROTECTED_COLOR = '#22c55e';
const UNPROTECTED_COLOR = '#e5e5e5';
const DONUT_WIDTH = 200;
const DAY_MS = 86_400_000;
export type DeviceDetail = {
name: string;
icon: ImageSourcePropType;
platform: string;
/** ISO — Bindungs-/Verbindungsdatum */
createdAt: string;
lastSeenAt?: string | null;
statusLabel: string;
statusColor: string;
};
function fmtDate(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, {
day: '2-digit',
month: 'short',
year: 'numeric',
});
}
function fmtLastSeen(iso: string, t: (k: string, o?: any) => string): string {
const min = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000);
if (min < 1) return t('settings.devices_just_now');
if (min < 60) return t('settings.devices_mins_ago', { count: min });
const hr = Math.floor(min / 60);
if (hr < 24) return t('settings.devices_hours_ago', { count: hr });
const day = Math.floor(hr / 24);
if (day < 30) return t('settings.devices_days_ago', { count: day });
return fmtDate(iso);
}
/**
* DeviceDetailSheet — Detail-Ansicht beim Tap auf eine Geräte-Row.
*
* Zeigt Verbindungsstatus, Bindungs-Datum und den Schutz-Verlauf als
* HalfDonut (geschützt vs. ungeschützt) — gleiche Visualisierung wie auf der
* Profile-Page. Per-Gerät-Split wird client-side berechnet:
* geschützt = volle Tage seit Bindung dieses Geräts
* ungeschützt = Account-Schutzalter, das VOR der Bindung lag
* → echte gerätespezifische Verteilung + Progress, ohne neuen Backend-Endpoint.
*/
export function DeviceDetailSheet({
visible,
device,
onClose,
}: {
visible: boolean;
device: DeviceDetail | null;
onClose: () => void;
}) {
const { t } = useTranslation();
const colors = useColors();
const { coverage } = useProtectionCoverage();
const { protectedDays, unprotectedDays } = useMemo(() => {
if (!device) return { protectedDays: 0, unprotectedDays: 0 };
const boundAt = new Date(device.createdAt).getTime();
const devProtected = Math.max(0, Math.floor((Date.now() - boundAt) / DAY_MS));
const accountSpan = coverage
? coverage.protectedDays + coverage.unprotectedDays
: devProtected;
const before = Math.max(0, accountSpan - devProtected);
return { protectedDays: devProtected, unprotectedDays: before };
}, [device, coverage]);
const segments = useMemo(
() => [
{ value: Math.max(protectedDays, 1), color: PROTECTED_COLOR },
{ value: Math.max(unprotectedDays, 0), color: UNPROTECTED_COLOR },
],
[protectedDays, unprotectedDays],
);
return (
<FormSheet
visible={visible}
onClose={onClose}
title={device?.name ?? ''}
initialHeightPct={0.6}
growWithKeyboard={false}
>
{device && (
<View style={{ paddingHorizontal: 20, paddingTop: 4, paddingBottom: 24, gap: 20 }}>
{/* Identität */}
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
<View
style={{
width: 48,
height: 48,
borderRadius: 14,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Image source={device.icon} style={{ width: 30, height: 30 }} resizeMode="contain" />
</View>
<View style={{ flex: 1, minWidth: 0 }}>
<Text
numberOfLines={1}
style={{ fontSize: 17, color: colors.text, fontFamily: 'Nunito_700Bold' }}
>
{device.name}
</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 3 }}>
<View
style={{ width: 7, height: 7, borderRadius: 4, backgroundColor: device.statusColor }}
/>
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}>
{device.statusLabel}
</Text>
</View>
</View>
</View>
{/* Schutz-Verlauf (HalfDonut, wie Profile) */}
<View
style={{
backgroundColor: colors.card,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 14,
padding: 16,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<Ionicons name="shield-checkmark-outline" size={14} color={colors.textMuted} />
<Text
style={{
fontSize: 11,
color: colors.textMuted,
fontFamily: 'Nunito_700Bold',
letterSpacing: 0.8,
textTransform: 'uppercase',
}}
>
{t('devices.detail_coverage_label')}
</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 16 }}>
<HalfDonut
segments={segments}
centerValue={protectedDays}
centerLabel={t('profile.coverage_center_label')}
width={DONUT_WIDTH}
/>
<View style={{ flex: 1, gap: 8, paddingBottom: 8 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: PROTECTED_COLOR }} />
<Text style={{ fontSize: 12, fontFamily: 'Nunito_600SemiBold', color: colors.text }}>
{protectedDays} {t('devices.detail_this_device_protected')}
</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<View
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: UNPROTECTED_COLOR,
borderWidth: 1,
borderColor: colors.border,
}}
/>
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{unprotectedDays} {t('devices.detail_before_binding')}
</Text>
</View>
</View>
</View>
</View>
{/* Meta-Infos */}
<View
style={{
backgroundColor: colors.card,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 14,
overflow: 'hidden',
}}
>
<InfoRow
label={t('devices.detail_connection')}
value={device.statusLabel}
valueColor={device.statusColor}
colors={colors}
/>
<InfoRow
label={t('devices.detail_added')}
value={fmtDate(device.createdAt)}
colors={colors}
divider
/>
{device.lastSeenAt ? (
<InfoRow
label={t('devices.detail_last_seen')}
value={fmtLastSeen(device.lastSeenAt, t)}
colors={colors}
divider
/>
) : null}
</View>
</View>
)}
</FormSheet>
);
}
function InfoRow({
label,
value,
valueColor,
colors,
divider,
}: {
label: string;
value: string;
valueColor?: string;
colors: ReturnType<typeof useColors>;
divider?: boolean;
}) {
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 14,
paddingVertical: 13,
borderTopWidth: divider ? 1 : 0,
borderTopColor: colors.border,
}}
>
<Text style={{ fontSize: 14, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
{label}
</Text>
<Text
style={{
fontSize: 14,
color: valueColor ?? colors.text,
fontFamily: 'Nunito_600SemiBold',
}}
>
{value}
</Text>
</View>
);
}