- 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
400 lines
11 KiB
TypeScript
400 lines
11 KiB
TypeScript
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',
|
||
},
|
||
});
|