Files
fiberops-mobile/components/SlideToConfirm.tsx

126 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from 'react';
import { View, Text, Animated, StyleSheet } from 'react-native';
const TRACK_HEIGHT = 60;
const HANDLE_SIZE = 48;
const PADDING = 6;
const THRESHOLD = 0.82;
interface Props {
label?: string;
color?: string;
onConfirm: () => void;
disabled?: boolean;
}
export function SlideToConfirm({ label = 'Slide to confirm', color = '#059669', onConfirm, disabled = false }: Props) {
const [trackW, setTrackW] = useState(0);
const [done, setDone] = useState(false);
const translateX = useRef(new Animated.Value(0)).current;
const startX = useRef(0);
const currentX = useRef(0);
const maxX = Math.max(trackW - HANDLE_SIZE - PADDING * 2, 1);
const onTouchStart = (e: any) => {
if (disabled || done) return;
startX.current = e.nativeEvent.pageX;
};
const onTouchMove = (e: any) => {
if (disabled || done) return;
const dx = e.nativeEvent.pageX - startX.current;
const x = Math.max(0, Math.min(dx, maxX));
currentX.current = x;
translateX.setValue(x);
};
const onTouchEnd = () => {
if (disabled || done) return;
if (maxX > 0 && currentX.current / maxX >= THRESHOLD) {
Animated.timing(translateX, { toValue: maxX, duration: 100, useNativeDriver: true }).start(() => {
setDone(true);
onConfirm();
});
} else {
Animated.spring(translateX, { toValue: 0, useNativeDriver: true, speed: 25, bounciness: 4 }).start();
currentX.current = 0;
}
};
// Fade label as handle moves right
const safeMax = Math.max(maxX * 0.5, 1);
const labelOpacity = translateX.interpolate({
inputRange: [0, safeMax],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<View
onLayout={e => setTrackW(e.nativeEvent.layout.width)}
style={[styles.track, {
backgroundColor: done ? color : '#F1F5F9',
borderColor: done ? color : '#E2E8F0',
}]}
>
{/* Label */}
<Animated.Text style={[styles.label, {
opacity: disabled ? 0.35 : labelOpacity,
color: done ? '#fff' : '#64748B',
}]}>
{done ? '✓ Confirmed!' : label}
</Animated.Text>
{/* Handle — touch events here */}
{!done && (
<Animated.View
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
style={[styles.handle, {
backgroundColor: disabled ? '#CBD5E1' : color,
transform: [{ translateX }],
}]}
>
<Text style={{ color: '#fff', fontSize: 22, fontWeight: '700', marginLeft: 2 }}></Text>
</Animated.View>
)}
</View>
);
}
const styles = StyleSheet.create({
track: {
height: TRACK_HEIGHT,
borderRadius: TRACK_HEIGHT / 2,
borderWidth: 1.5,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
label: {
fontSize: 15,
fontWeight: '700',
letterSpacing: 0.3,
textAlign: 'center',
paddingLeft: HANDLE_SIZE + PADDING * 2,
paddingRight: 16,
},
handle: {
position: 'absolute',
left: PADDING,
top: PADDING,
width: HANDLE_SIZE,
height: HANDLE_SIZE,
borderRadius: HANDLE_SIZE / 2,
alignItems: 'center',
justifyContent: 'center',
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
});