133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';
|
|
import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg';
|
|
|
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
|
|
export type HealthProgressRingProps = {
|
|
progress: number; // 0-100
|
|
size?: number;
|
|
strokeWidth?: number;
|
|
gradientColors?: string[];
|
|
label?: string;
|
|
suffix?: string;
|
|
title: string;
|
|
};
|
|
|
|
export function HealthProgressRing({
|
|
progress,
|
|
size = 80,
|
|
strokeWidth = 8,
|
|
gradientColors = ['#5B4CFF', '#9B8AFB'],
|
|
label,
|
|
suffix = '%',
|
|
title,
|
|
}: HealthProgressRingProps) {
|
|
const animatedProgress = useRef(new Animated.Value(0)).current;
|
|
const radius = (size - strokeWidth) / 2;
|
|
const circumference = 2 * Math.PI * radius;
|
|
const center = size / 2;
|
|
|
|
useEffect(() => {
|
|
Animated.timing(animatedProgress, {
|
|
toValue: progress,
|
|
duration: 1000,
|
|
easing: Easing.out(Easing.cubic),
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}, [progress]);
|
|
|
|
const strokeDashoffset = animatedProgress.interpolate({
|
|
inputRange: [0, 100],
|
|
outputRange: [circumference, 0],
|
|
extrapolate: 'clamp',
|
|
});
|
|
|
|
const gradientId = useRef(`grad-${Math.random().toString(36).substr(2, 9)}`).current;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
|
|
<Svg width={size} height={size}>
|
|
<Defs>
|
|
<LinearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
|
|
<Stop offset="0" stopColor={gradientColors[0]} stopOpacity="1" />
|
|
<Stop offset="1" stopColor={gradientColors[1]} stopOpacity="1" />
|
|
</LinearGradient>
|
|
</Defs>
|
|
|
|
{/* Background Circle */}
|
|
<Circle
|
|
cx={center}
|
|
cy={center}
|
|
r={radius}
|
|
stroke="#F3F4F6"
|
|
strokeWidth={strokeWidth}
|
|
fill="none"
|
|
/>
|
|
|
|
{/* Progress Circle */}
|
|
<AnimatedCircle
|
|
cx={center}
|
|
cy={center}
|
|
r={radius}
|
|
stroke={`url(#${gradientId})`}
|
|
strokeWidth={strokeWidth}
|
|
fill="none"
|
|
strokeDasharray={circumference}
|
|
strokeDashoffset={strokeDashoffset}
|
|
strokeLinecap="round"
|
|
transform={`rotate(-90 ${center} ${center})`}
|
|
/>
|
|
</Svg>
|
|
|
|
<View style={styles.centerContent}>
|
|
<View style={styles.valueContainer}>
|
|
<Text style={styles.valueText}>{label ?? progress}</Text>
|
|
<Text style={styles.suffixText}>{suffix}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.titleText}>{title}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
centerContent: {
|
|
position: 'absolute',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
valueContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-end',
|
|
},
|
|
valueText: {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
color: '#1F2937',
|
|
fontFamily: 'AliBold',
|
|
lineHeight: 24,
|
|
},
|
|
suffixText: {
|
|
fontSize: 12,
|
|
color: '#6B7280',
|
|
fontWeight: '500',
|
|
marginLeft: 1,
|
|
marginBottom: 3,
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
titleText: {
|
|
marginTop: 8,
|
|
fontSize: 14,
|
|
color: '#4B5563', // gray-600
|
|
fontFamily: 'AliRegular',
|
|
},
|
|
});
|