Files
digital-pilates/components/fasting/FastingOverviewCard.tsx
richarjiang d39a32c0d8 feat(fasting): add auto-renewal and reset functionality for fasting plans
- Implement auto-renewal logic for completed fasting cycles using dayjs
- Add reset button with information modal in FastingOverviewCard
- Configure iOS push notifications for production environment
- Add expo-media-library and react-native-view-shot dependencies
- Update FastingScheduleOrigin type to include 'auto' origin
2025-10-15 19:06:18 +08:00

400 lines
11 KiB
TypeScript
Raw Permalink 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 { CircularRing } from '@/components/CircularRing';
import { Colors } from '@/constants/Colors';
import type { FastingPlan } from '@/constants/Fasting';
import { useColorScheme } from '@/hooks/useColorScheme';
import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react';
import {
Modal,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
type FastingOverviewCardProps = {
plan?: FastingPlan;
phaseLabel: string;
countdownLabel: string;
countdownValue: string;
startDayLabel: string;
startTimeLabel: string;
endDayLabel: string;
endTimeLabel: string;
onAdjustStartPress: () => void;
onViewMealsPress: () => void;
onResetPress: () => void;
progress: number;
};
export function FastingOverviewCard({
plan,
phaseLabel,
countdownLabel,
countdownValue,
startDayLabel,
startTimeLabel,
endDayLabel,
endTimeLabel,
onAdjustStartPress,
onViewMealsPress,
onResetPress,
progress,
}: FastingOverviewCardProps) {
const theme = useColorScheme() ?? 'light';
const colors = Colors[theme];
const themeColors = plan?.theme;
const [showResetInfo, setShowResetInfo] = useState(false);
const isGlassAvailable = isLiquidGlassAvailable();
return (
<>
<LinearGradient
colors={[
themeColors?.accentSecondary ?? colors.heroSurfaceTint,
themeColors?.backdrop ?? colors.pageBackgroundEmphasis,
]}
style={styles.container}
>
<View style={styles.headerRow}>
<View>
<Text style={styles.planLabel}></Text>
{plan?.id && (
<View style={styles.planTag}>
<Text style={[styles.planTagText, { color: themeColors?.accent ?? colors.primary }]}>
{plan.id}
</Text>
</View>
)}
</View>
{plan?.badge && (
<View style={[styles.badge, { backgroundColor: `${themeColors?.accent ?? colors.primary}20` }]}>
<Text style={[styles.badgeText, { color: themeColors?.accent ?? colors.primary }]}>
{plan.badge}
</Text>
</View>
)}
</View>
<View style={styles.scheduleRow}>
<View style={styles.scheduleCell}>
<Text style={styles.scheduleLabel}></Text>
<Text style={styles.scheduleDay}>{startDayLabel}</Text>
<Text style={styles.scheduleTime}>{startTimeLabel}</Text>
</View>
<View style={styles.separator} />
<View style={styles.scheduleCell}>
<Text style={styles.scheduleLabel}></Text>
<Text style={styles.scheduleDay}>{endDayLabel}</Text>
<Text style={styles.scheduleTime}>{endTimeLabel}</Text>
</View>
</View>
<View style={styles.statusRow}>
<View style={styles.ringContainer}>
<CircularRing
size={168}
strokeWidth={14}
progress={progress}
progressColor={themeColors?.ringProgress ?? colors.primary}
trackColor={themeColors?.ringTrack ?? 'rgba(0,0,0,0.05)'}
showCenterText={false}
startAngleDeg={-90}
resetToken={phaseLabel}
/>
<View style={styles.ringContent}>
<Text style={styles.phaseText}>{phaseLabel}</Text>
<Text style={styles.countdownLabel}>{countdownLabel}</Text>
<Text style={styles.countdownValue}>{countdownValue}</Text>
</View>
</View>
</View>
<View style={styles.actionsRow}>
<TouchableOpacity
style={[styles.secondaryButton, { borderColor: themeColors?.accent ?? colors.primary }]}
onPress={onAdjustStartPress}
activeOpacity={0.85}
>
<Text style={[styles.secondaryButtonText, { color: themeColors?.accent ?? colors.primary }]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.secondaryButton, { borderColor: themeColors?.accent ?? colors.primary }]}
onPress={onResetPress}
activeOpacity={0.85}
>
<Text style={[styles.secondaryButtonText, { color: themeColors?.accent ?? colors.primary }]}>
</Text>
</TouchableOpacity>
{isGlassAvailable ? (
<GlassView
style={styles.infoButton}
glassEffectStyle="regular"
isInteractive={true}
>
<TouchableOpacity onPress={() => setShowResetInfo(true)} style={styles.infoButtonInner}>
<Ionicons name="information-circle-outline" size={20} color={themeColors?.accent ?? colors.primary} />
</TouchableOpacity>
</GlassView>
) : (
<TouchableOpacity
onPress={() => setShowResetInfo(true)}
style={[styles.infoButton, styles.fallbackInfoButton]}
>
<Ionicons name="information-circle-outline" size={20} color={themeColors?.accent ?? colors.primary} />
</TouchableOpacity>
)}
</View>
</LinearGradient>
<Modal
transparent
visible={showResetInfo}
animationType="fade"
onRequestClose={() => setShowResetInfo(false)}
>
<Pressable
style={styles.infoModalOverlay}
onPress={() => setShowResetInfo(false)}
>
<Pressable style={styles.infoModalContent} onPress={() => { }}>
<View style={styles.infoModalHandle} />
<Text style={styles.infoModalTitle}></Text>
<Text style={styles.infoModalText}>
</Text>
<Text style={styles.infoModalText}>
</Text>
<TouchableOpacity
style={[styles.infoModalButton, { backgroundColor: themeColors?.accent ?? colors.primary }]}
onPress={() => setShowResetInfo(false)}
>
<Text style={styles.infoModalButtonText}></Text>
</TouchableOpacity>
</Pressable>
</Pressable>
</Modal>
</>
);
}
const styles = StyleSheet.create({
container: {
borderRadius: 28,
paddingHorizontal: 20,
paddingVertical: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 16 },
shadowOpacity: 0.08,
shadowRadius: 24,
elevation: 6,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
planLabel: {
fontSize: 18,
fontWeight: '700',
color: '#2E3142',
marginBottom: 6,
},
planTag: {
alignSelf: 'flex-start',
backgroundColor: 'rgba(255,255,255,0.8)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
planTagText: {
fontSize: 13,
fontWeight: '600',
},
badge: {
paddingHorizontal: 14,
paddingVertical: 6,
borderRadius: 18,
},
badgeText: {
fontSize: 12,
fontWeight: '600',
},
scheduleRow: {
flexDirection: 'row',
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.8)',
paddingVertical: 14,
paddingHorizontal: 16,
alignItems: 'center',
},
scheduleCell: {
flex: 1,
alignItems: 'center',
},
scheduleLabel: {
fontSize: 13,
color: '#70808E',
marginBottom: 6,
fontWeight: '500',
},
scheduleDay: {
fontSize: 16,
color: '#2E3142',
fontWeight: '600',
},
scheduleTime: {
fontSize: 24,
fontWeight: '700',
color: '#2E3142',
marginTop: 4,
},
separator: {
width: 1,
height: 52,
backgroundColor: 'rgba(112,128,142,0.22)',
},
statusRow: {
marginTop: 26,
alignItems: 'center',
},
ringContainer: {
width: 180,
height: 180,
borderRadius: 90,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
position: 'relative',
},
ringContent: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
phaseText: {
fontSize: 18,
fontWeight: '700',
color: '#2E3142',
marginBottom: 8,
},
countdownLabel: {
fontSize: 12,
color: '#6F7D87',
marginBottom: 4,
},
countdownValue: {
fontSize: 20,
fontWeight: '700',
color: '#2E3142',
letterSpacing: 1,
},
actionsRow: {
marginTop: 24,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
secondaryButton: {
paddingHorizontal: 20,
borderWidth: 1.2,
borderRadius: 24,
paddingVertical: 14,
marginRight: 12,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255,255,255,0.92)',
},
secondaryButtonText: {
fontSize: 15,
fontWeight: '600',
},
infoButton: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 8,
},
infoButtonInner: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
},
fallbackInfoButton: {
backgroundColor: 'rgba(255,255,255,0.3)',
},
infoModalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
infoModalContent: {
backgroundColor: 'white',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingVertical: 24,
paddingHorizontal: 20,
paddingBottom: 40,
},
infoModalHandle: {
width: 36,
height: 4,
backgroundColor: '#E5E7EB',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 16,
},
infoModalTitle: {
fontSize: 18,
fontWeight: '700',
color: '#2E3142',
marginBottom: 16,
textAlign: 'center',
},
infoModalText: {
fontSize: 15,
color: '#4A5460',
lineHeight: 22,
marginBottom: 12,
},
infoModalButton: {
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
},
infoModalButtonText: {
fontSize: 15,
fontWeight: '600',
color: '#FFFFFF',
},
primaryButton: {
flex: 1,
borderRadius: 24,
paddingVertical: 14,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#2E3142',
},
primaryButtonText: {
fontSize: 15,
fontWeight: '700',
color: '#fff',
},
});