chahinebrini e2e5a1003c feat(native): Geräte-Slots als Progress-Ringe + Status-Pill in der Liste
- Slots: zwei animierte volle Progress-Circles (Mobil/Computer) statt Balken,
  via react-native-svg (keine neue Lib)
- Status-Zeile pro Gerät: Online (grün) / Cooldown · noch Xh (amber, aus
  releaseRequestedAt) / Ungeschützt (rot) — ersetzt Footer + StatusBadge

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:17:05 +02:00

109 lines
2.9 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { Animated, Easing, Text, View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import { useColors } from '../../lib/theme';
const SIZE = 104;
const STROKE = 9;
const R = (SIZE - STROKE) / 2;
const C = 2 * Math.PI * R;
/**
* Voller Progress-Ring (kein Half-Donut) für belegte/freie Geräte-Slots.
* Mit react-native-svg (schon im Projekt) statt neuer Lib — animiert über
* strokeDashoffset. Zwei nebeneinander (Mobil + Computer).
*/
export function DeviceSlotDonut({
count,
max,
atLimit,
label,
}: {
count: number;
max: number;
atLimit: boolean;
label: string;
}) {
const colors = useColors();
const ratio = max > 0 ? Math.min(count / max, 1) : 0;
const fill = atLimit ? colors.brandOrange : colors.success;
const anim = useRef(new Animated.Value(0)).current;
const [progress, setProgress] = useState(0);
useEffect(() => {
anim.setValue(0);
const l = anim.addListener(({ value }) => setProgress(value));
Animated.timing(anim, {
toValue: 1,
duration: 950,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
return () => anim.removeListener(l);
}, [ratio, anim]);
const offset = C * (1 - ratio * progress);
return (
<View style={{ alignItems: 'center', flex: 1 }}>
<View style={{ width: SIZE, height: SIZE }}>
<Svg width={SIZE} height={SIZE}>
<Circle
cx={SIZE / 2}
cy={SIZE / 2}
r={R}
stroke={colors.surfaceElevated}
strokeWidth={STROKE}
fill="none"
/>
<Circle
cx={SIZE / 2}
cy={SIZE / 2}
r={R}
stroke={fill}
strokeWidth={STROKE}
fill="none"
strokeDasharray={`${C} ${C}`}
strokeDashoffset={offset}
strokeLinecap="round"
transform={`rotate(-90 ${SIZE / 2} ${SIZE / 2})`}
/>
</Svg>
<View
pointerEvents="none"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ fontSize: 26, fontFamily: 'Nunito_900Black', color: colors.text, letterSpacing: -0.5 }}>
{count}
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.textMuted }}>
/{max}
</Text>
</Text>
</View>
</View>
<Text
numberOfLines={1}
style={{
fontSize: 11,
color: atLimit ? colors.brandOrange : colors.textMuted,
fontFamily: 'Nunito_600SemiBold',
marginTop: 6,
textAlign: 'center',
}}
>
{label}
</Text>
</View>
);
}