import { memo, useCallback, useMemo } from 'react'; import { Text } from '@/aesthetics/Text'; import { buttonHeight, font, radius, spacing, useColors } from '@/aesthetics/styles'; import { Check, X } from 'lucide-react-native'; import { StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { FadeInDown, FadeOut, Layout, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import { useHaptics } from '@/aesthetics/useHaptics'; import { useStrings } from '@/common/hooks/useStrings'; import { TYPE_ICONS, SWIPE_THRESHOLD, formatValue } from './constants'; import { getTimeAgo } from '@/common/utils/format'; import type { MemoLog } from './types'; type MemoLogCardProps = { entry: MemoLog; onRequestDelete: () => void; onPress: () => void; index: number; }; export const MemoLogCard = memo(function MemoLogCard({ entry, onRequestDelete, onPress, index, }: MemoLogCardProps) { const c = useColors(); const strings = useStrings(); const { notification, impact, NotificationType, ImpactStyle } = useHaptics(); const getDisplayValue = (e: MemoLog): string | null => { if (e.type === 'check') return e.value === 1 ? strings.userCustomLogs.yes : strings.userCustomLogs.no; if (e.type === 'number') return formatValue(e.value, e.valueUnit); return String(e.value); }; const translateX = useSharedValue(0); const hasPassedThreshold = useSharedValue(false); const IconComponent = TYPE_ICONS[entry.type]; const triggerDeleteConfirm = useCallback(() => { notification(NotificationType.Success); onRequestDelete(); }, [notification, NotificationType, onRequestDelete]); const triggerHaptic = useCallback(() => { impact(ImpactStyle.Medium); }, [impact, ImpactStyle]); const tapGesture = useMemo( () => Gesture.Tap().onEnd(() => { 'worklet'; runOnJS(onPress)(); }), [onPress] ); const panGesture = useMemo( () => Gesture.Pan() .activeOffsetX([-10, 10]) .failOffsetY([-20, 20]) .onUpdate(e => { 'worklet'; const clampedX = Math.min(0, Math.max(-120, e.translationX)); translateX.value = clampedX; if (clampedX <= -SWIPE_THRESHOLD && !hasPassedThreshold.value) { hasPassedThreshold.value = true; runOnJS(triggerHaptic)(); } else if (clampedX > -SWIPE_THRESHOLD) { hasPassedThreshold.value = false; } }) .onEnd(e => { 'worklet'; hasPassedThreshold.value = false; if (e.translationX <= -SWIPE_THRESHOLD) { runOnJS(triggerDeleteConfirm)(); } else { translateX.value = withTiming(0, { duration: 200 }); } }), [translateX, hasPassedThreshold, triggerDeleteConfirm, triggerHaptic] ); const composedGesture = useMemo( () => Gesture.Race(tapGesture, panGesture), [tapGesture, panGesture] ); const rowStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], })); const deleteStyle = useAnimatedStyle(() => ({ opacity: Math.min(1, Math.abs(translateX.value) / SWIPE_THRESHOLD), })); const timeAgo = getTimeAgo(entry.timestamp); return ( {strings.userCustomLogs.delete} {entry.icon ? ( {entry.icon} ) : entry.type === 'check' ? ( entry.value === 1 ? ( ) : ( ) ) : IconComponent ? ( ) : null} {entry.title} {getDisplayValue(entry)} {timeAgo} ); }); const styles = StyleSheet.create({ swipeContainer: { position: 'relative', overflow: 'hidden', borderRadius: radius.md }, swipeDeleteBg: { position: 'absolute', right: 0, top: 0, bottom: 0, width: 120, alignItems: 'center', justifyContent: 'center', borderTopRightRadius: radius.md, borderBottomRightRadius: radius.md, }, swipeDeleteText: { fontSize: font.sm, fontWeight: '600' }, entryRow: { flexDirection: 'row', alignItems: 'center', minHeight: buttonHeight.md, borderRadius: radius.md, overflow: 'hidden', }, entryIconWrap: { width: 32, alignItems: 'center', justifyContent: 'center', marginLeft: spacing.md, }, entryIcon: { fontSize: 16 }, entryContent: { flex: 1, paddingVertical: spacing.sm, paddingHorizontal: spacing.sm }, entryName: { fontSize: font.sm, fontWeight: '500' }, entryValue: { fontSize: font.xs, marginTop: 2 }, entryTime: { fontSize: font.xs, marginRight: spacing.md }, });