Files
digital-pilates/app/weight-records.tsx

875 lines
26 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 NumberKeyboard from '@/components/NumberKeyboard';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { WeightProgressBar } from '@/components/weight/WeightProgressBar';
import { WeightRecordCard } from '@/components/weight/WeightRecordCard';
import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { appStoreReviewService } from '@/services/appStoreReview';
import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useCallback, useEffect, useState } from 'react';
import {
Alert,
Image,
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
export default function WeightRecordsPage() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
const dispatch = useAppDispatch();
const userProfile = useAppSelector((s) => s.user.profile);
const weightHistory = useAppSelector((s) => s.user.weightHistory);
const [showWeightPicker, setShowWeightPicker] = useState(false);
const [pickerType, setPickerType] = useState<'current' | 'initial' | 'target' | 'edit'>('current');
const [inputWeight, setInputWeight] = useState('');
const [editingRecord, setEditingRecord] = useState<WeightHistoryItem | null>(null);
const colorScheme = useColorScheme();
const themeColors = Colors[colorScheme ?? 'light'];
const { isLoggedIn, ensureLoggedIn } = useAuthGuard();
const loadWeightHistory = useCallback(async () => {
if (!isLoggedIn) return;
try {
await dispatch(fetchWeightHistory() as any);
} catch (error) {
console.error(t('weightRecords.loadingHistory'), error);
}
}, [dispatch, isLoggedIn]);
useEffect(() => {
loadWeightHistory();
}, [loadWeightHistory]);
const initializeInput = (weight: number) => {
setInputWeight(weight.toString());
};
const handleAddWeight = async () => {
const ok = await ensureLoggedIn();
if (!ok) return;
setPickerType('current');
const weight = userProfile?.weight ? parseFloat(userProfile.weight) : 70.0;
initializeInput(weight);
setShowWeightPicker(true);
};
const handleEditInitialWeight = async () => {
const ok = await ensureLoggedIn();
if (!ok) return;
setPickerType('initial');
const initialWeight = userProfile?.initialWeight || userProfile?.weight || '70.0';
initializeInput(parseFloat(initialWeight));
setShowWeightPicker(true);
};
const handleEditTargetWeight = async () => {
const ok = await ensureLoggedIn();
if (!ok) return;
setPickerType('target');
const targetWeight = userProfile?.targetWeight || '60.0';
initializeInput(parseFloat(targetWeight));
setShowWeightPicker(true);
};
const handleEditWeightRecord = async (record: WeightHistoryItem) => {
const ok = await ensureLoggedIn();
if (!ok) return;
setPickerType('edit');
setEditingRecord(record);
initializeInput(parseFloat(record.weight));
setShowWeightPicker(true);
};
const handleDeleteWeightRecord = async (id: string) => {
const ok = await ensureLoggedIn();
if (!ok) return;
try {
await dispatch(deleteWeightRecord(id) as any);
await loadWeightHistory();
} catch (error) {
console.error(t('weightRecords.alerts.deleteFailed'), error);
Alert.alert('错误', t('weightRecords.alerts.deleteFailed'));
}
};
const handleWeightSave = async () => {
const weight = parseFloat(inputWeight);
if (isNaN(weight) || weight <= 0 || weight > 500) {
alert(t('weightRecords.alerts.invalidWeight'));
return;
}
try {
if (pickerType === 'current') {
// Update current weight in profile and add weight record
await dispatch(updateUserProfile({ weight: weight }) as any);
// 记录体重后尝试请求应用评分延迟1秒避免阻塞主流程
setTimeout(() => {
appStoreReviewService.requestReview().catch((error) => {
console.error('应用评分请求失败:', error);
});
}, 1000);
} else if (pickerType === 'initial') {
// Update initial weight in profile
console.log('更新初始体重');
await dispatch(updateUserProfile({ initialWeight: weight }) as any);
} else if (pickerType === 'target') {
// Update target weight in profile
await dispatch(updateUserProfile({ targetWeight: weight }) as any);
} else if (pickerType === 'edit' && editingRecord) {
await dispatch(updateWeightRecord({ id: editingRecord.id, weight }) as any);
}
setShowWeightPicker(false);
setInputWeight('');
setEditingRecord(null);
await loadWeightHistory();
} catch (error) {
console.error(t('weightRecords.alerts.saveFailed'), error);
Alert.alert('错误', t('weightRecords.alerts.saveFailed'));
}
};
const handleNumberPress = (number: string) => {
setInputWeight(prev => {
// 防止输入多个0开头
if (prev === '0' && number === '0') return prev;
// 如果当前是0输入非0数字时替换
if (prev === '0' && number !== '0') return number;
return prev + number;
});
};
const handleDeletePress = () => {
setInputWeight(prev => prev.slice(0, -1));
};
const handleDecimalPress = () => {
setInputWeight(prev => {
if (prev.includes('.')) return prev;
// 如果没有输入任何数字自动添加0
if (!prev) return '0.';
return prev + '.';
});
};
// Process weight history data
const sortedHistory = [...weightHistory]
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
// Group by month
const groupedHistory = sortedHistory.reduce((acc, item) => {
const date = dayjs(item.createdAt);
const monthKey = t('weightRecords.historyMonthFormat', {
year: date.format('YYYY'),
month: date.format('MM')
});
if (!acc[monthKey]) {
acc[monthKey] = [];
}
acc[monthKey].push(item);
return acc;
}, {} as Record<string, WeightHistoryItem[]>);
// Calculate statistics
const currentWeight = userProfile?.weight ? parseFloat(userProfile.weight) : 0;
const initialWeight = userProfile?.initialWeight ? parseFloat(userProfile.initialWeight) :
(sortedHistory.length > 0 ? parseFloat(sortedHistory[sortedHistory.length - 1].weight) : currentWeight);
const targetWeight = userProfile?.targetWeight ? parseFloat(userProfile.targetWeight) : 60.0;
const totalWeightLoss = initialWeight - currentWeight;
// 计算减重进度
const hasTargetWeight = targetWeight > 0 && initialWeight > targetWeight;
const totalToLose = initialWeight - targetWeight;
const actualLost = initialWeight - currentWeight;
const weightProgress = hasTargetWeight && totalToLose > 0 ? actualLost / totalToLose : 0;
return (
<View style={styles.container}>
{/* 背景 */}
<LinearGradient
colors={['#f3f4fb', '#f3f4fb']}
style={StyleSheet.absoluteFillObject}
/>
{/* 顶部装饰性渐变 */}
<LinearGradient
colors={['rgba(229, 252, 254, 0.8)', 'rgba(243, 244, 251, 0)']}
style={styles.topGradient}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
<HeaderBar
title={t('weightRecords.title')}
right={
isLiquidGlassAvailable() ? (
<TouchableOpacity
onPress={handleAddWeight}
activeOpacity={0.7}
>
<GlassView
style={styles.addButtonGlass}
glassEffectStyle="regular"
tintColor="rgba(255, 255, 255, 0.4)"
isInteractive={true}
>
<Ionicons name="add" size={24} color="#1c1f3a" />
</GlassView>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.addButtonFallback}
onPress={handleAddWeight}
activeOpacity={0.7}
>
<Ionicons name="add" size={24} color="#1c1f3a" />
</TouchableOpacity>
)
}
/>
<ScrollView
style={styles.content}
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20, paddingTop: safeAreaTop }]}
showsVerticalScrollIndicator={false}
>
<View style={styles.headerBlock}>
<Text style={styles.pageTitle}>{t('weightRecords.title')}</Text>
<Text style={styles.pageSubtitle}>{t('weightRecords.pageSubtitle')}</Text>
</View>
{/* Weight Statistics Cards */}
<View style={styles.statsGrid}>
{/* Current Weight - Hero Card */}
<View style={styles.mainStatCard}>
<View style={styles.mainStatContent}>
<Text style={styles.mainStatLabel}>{t('weightRecords.stats.currentWeight')}</Text>
<View style={styles.mainStatValueContainer}>
<Text style={styles.mainStatValue}>{currentWeight.toFixed(1)}</Text>
<Text style={styles.mainStatUnit}>kg</Text>
</View>
<View style={styles.totalLossTag}>
<Ionicons name={totalWeightLoss > 0 ? "trending-down" : "trending-up"} size={16} color="#ffffff" />
<Text style={styles.totalLossText}>
{totalWeightLoss > 0 ? '-' : totalWeightLoss < 0 ? '+' : ''}{Math.abs(totalWeightLoss).toFixed(1)} kg
</Text>
</View>
</View>
<LinearGradient
colors={['#4F5BD5', '#6B6CFF']}
style={StyleSheet.absoluteFillObject}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
// @ts-ignore
borderRadius={24}
/>
<Image
source={require('@/assets/images/icons/iconWeight.png')}
style={styles.statIconBg}
/>
</View>
{/* Secondary Stats Row */}
<View style={styles.secondaryStatsRow}>
{/* Initial Weight */}
<TouchableOpacity
style={styles.secondaryStatCard}
onPress={handleEditInitialWeight}
activeOpacity={0.7}
>
<View style={styles.secondaryStatHeader}>
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.initialWeight')}</Text>
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
</View>
<Text style={styles.secondaryStatValue}>{initialWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
</TouchableOpacity>
{/* Target Weight */}
<TouchableOpacity
style={styles.secondaryStatCard}
onPress={handleEditTargetWeight}
activeOpacity={0.7}
>
<View style={styles.secondaryStatHeader}>
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.targetWeight')}</Text>
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
</View>
<Text style={styles.secondaryStatValue}>{targetWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
</TouchableOpacity>
</View>
</View>
{/* 减重进度条 - 仅在设置了目标体重时显示 */}
{hasTargetWeight && (
<View style={styles.progressContainer}>
<WeightProgressBar
progress={weightProgress}
currentWeight={currentWeight}
targetWeight={targetWeight}
initialWeight={initialWeight}
showTopBorder={false}
/>
</View>
)}
{/* Monthly Records */}
{Object.keys(groupedHistory).length > 0 ? (
<View style={styles.historySection}>
<Text style={styles.sectionTitle}>{t('weightRecords.history')}</Text>
{Object.entries(groupedHistory).map(([month, records]) => (
<View key={month} style={styles.monthContainer}>
<View style={styles.monthHeader}>
<Text style={styles.monthTitle}>{month}</Text>
</View>
{/* Individual Record Cards */}
<View style={styles.recordsList}>
{records.map((record, recordIndex) => {
// Calculate weight change from previous record
const prevRecord = recordIndex < records.length - 1 ? records[recordIndex + 1] : null;
const weightChange = prevRecord ?
parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0;
return (
<WeightRecordCard
key={`${record.createdAt}-${recordIndex}`}
record={record}
onPress={handleEditWeightRecord}
onDelete={handleDeleteWeightRecord}
weightChange={weightChange}
/>
);
})}
</View>
</View>
))}
</View>
) : (
<View style={styles.emptyContainer}>
<Image
source={require('@/assets/images/icons/iconWeight.png')}
style={{ width: 80, height: 80, opacity: 0.5, marginBottom: 16, tintColor: '#cbd5e1' }}
/>
<View style={styles.emptyContent}>
<Text style={styles.emptyText}>{t('weightRecords.empty.title')}</Text>
<Text style={styles.emptySubtext}>{t('weightRecords.empty.subtitle')}</Text>
</View>
</View>
)}
</ScrollView>
{/* Weight Input Modal */}
<Modal
visible={showWeightPicker}
animationType="fade"
transparent
onRequestClose={() => setShowWeightPicker(false)}
>
<View style={styles.modalContainer}>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={() => setShowWeightPicker(false)}
/>
<View style={[styles.modalSheet, { backgroundColor: '#ffffff' }]}>
{/* Header */}
<View style={styles.modalHeader}>
<TouchableOpacity
onPress={() => setShowWeightPicker(false)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="close" size={24} color="#1c1f3a" />
</TouchableOpacity>
<Text style={styles.modalTitle}>
{pickerType === 'current' && t('weightRecords.modal.recordWeight')}
{pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')}
{pickerType === 'target' && t('weightRecords.modal.editTargetWeight')}
{pickerType === 'edit' && t('weightRecords.modal.editRecord')}
</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView
style={styles.modalContent}
showsVerticalScrollIndicator={false}
>
{/* Weight Display Section */}
<View style={styles.inputSection}>
<View style={styles.weightInputContainer}>
<View style={styles.weightIcon}>
<Image
source={require('@/assets/images/icons/iconWeight.png')}
style={{ width: 24, height: 24, tintColor: '#4F5BD5' }}
/>
</View>
<View style={styles.inputWrapper}>
<Text style={[
styles.weightDisplay,
{ color: inputWeight ? '#1c1f3a' : '#9ba3c7' }
]}>
{inputWeight || t('weightRecords.modal.inputPlaceholder')}
</Text>
<Text style={styles.unitLabel}>{t('weightRecords.modal.unit')}</Text>
</View>
</View>
</View>
{/* Quick Selection */}
<View style={styles.quickSelectionSection}>
<Text style={styles.quickSelectionTitle}>{t('weightRecords.modal.quickSelection')}</Text>
<View style={styles.quickButtons}>
{[50, 60, 70, 80, 90].map((weight) => (
<TouchableOpacity
key={weight}
style={[
styles.quickButton,
inputWeight === weight.toString() && styles.quickButtonSelected
]}
onPress={() => setInputWeight(weight.toString())}
activeOpacity={0.7}
>
<Text style={[
styles.quickButtonText,
inputWeight === weight.toString() && styles.quickButtonTextSelected
]}>
{weight}{t('weightRecords.modal.unit')}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</ScrollView>
{/* Custom Number Keyboard */}
<NumberKeyboard
onNumberPress={handleNumberPress}
onDeletePress={handleDeletePress}
onDecimalPress={handleDecimalPress}
hasDecimal={inputWeight.includes('.')}
maxLength={6}
currentValue={inputWeight}
/>
{/* Save Button */}
<View style={styles.modalFooter}>
<TouchableOpacity
style={[
styles.saveButton,
{ opacity: !inputWeight.trim() ? 0.5 : 1 }
]}
onPress={handleWeightSave}
disabled={!inputWeight.trim()}
activeOpacity={0.8}
>
<LinearGradient
colors={['#4F5BD5', '#6B6CFF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.saveButtonGradient}
>
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
topGradient: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: 300,
},
content: {
flex: 1,
},
contentContainer: {
flexGrow: 1,
paddingBottom: 40,
},
headerBlock: {
paddingHorizontal: 24,
marginTop: 10,
marginBottom: 24,
},
pageTitle: {
fontSize: 28,
fontWeight: '800',
color: '#1c1f3a',
fontFamily: 'AliBold',
marginBottom: 4,
},
pageSubtitle: {
fontSize: 16,
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
// Add Button Styles
addButtonGlass: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
overflow: 'hidden',
},
addButtonFallback: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
},
// Stats Grid
statsGrid: {
paddingHorizontal: 24,
marginBottom: 32,
gap: 16,
},
mainStatCard: {
backgroundColor: '#4F5BD5',
borderRadius: 28,
padding: 24,
height: 160,
position: 'relative',
overflow: 'hidden',
shadowColor: '#4F5BD5',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 8,
},
mainStatContent: {
zIndex: 2,
height: '100%',
justifyContent: 'space-between',
},
mainStatLabel: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.9)',
fontFamily: 'AliRegular',
},
mainStatValueContainer: {
flexDirection: 'row',
alignItems: 'baseline',
},
mainStatValue: {
fontSize: 48,
fontWeight: '800',
color: '#ffffff',
fontFamily: 'AliBold',
marginRight: 8,
},
mainStatUnit: {
fontSize: 20,
fontWeight: '600',
color: 'rgba(255, 255, 255, 0.9)',
fontFamily: 'AliRegular',
},
totalLossTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.2)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
alignSelf: 'flex-start',
},
totalLossText: {
fontSize: 14,
fontWeight: '600',
color: '#ffffff',
marginLeft: 4,
fontFamily: 'AliBold',
},
statIconBg: {
position: 'absolute',
right: -20,
bottom: -20,
width: 140,
height: 140,
opacity: 0.2,
transform: [{ rotate: '-15deg' }],
tintColor: '#ffffff'
},
secondaryStatsRow: {
flexDirection: 'row',
gap: 16,
},
secondaryStatCard: {
flex: 1,
backgroundColor: '#ffffff',
borderRadius: 24,
padding: 16,
shadowColor: 'rgba(30, 41, 59, 0.06)',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 3,
},
secondaryStatHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
secondaryStatLabel: {
fontSize: 13,
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
secondaryStatValue: {
fontSize: 20,
fontWeight: '700',
color: '#1c1f3a',
fontFamily: 'AliBold',
},
secondaryStatUnit: {
fontSize: 14,
color: '#6f7ba7',
fontWeight: '500',
fontFamily: 'AliRegular',
marginLeft: 2,
},
// Progress Container
progressContainer: {
marginHorizontal: 24,
marginBottom: 24,
backgroundColor: '#ffffff',
borderRadius: 24,
padding: 20,
shadowColor: 'rgba(30, 41, 59, 0.06)',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 3,
},
// History Section
historySection: {
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
color: '#1c1f3a',
marginBottom: 16,
fontFamily: 'AliBold',
},
monthContainer: {
marginBottom: 24,
},
monthHeader: {
marginBottom: 12,
paddingHorizontal: 4,
},
monthTitle: {
fontSize: 15,
fontWeight: '600',
color: '#6f7ba7',
fontFamily: 'AliRegular',
},
recordsList: {
gap: 12,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 60,
},
emptyContent: {
alignItems: 'center',
},
emptyText: {
fontSize: 16,
fontWeight: '700',
color: '#1c1f3a',
marginBottom: 8,
fontFamily: 'AliBold',
},
emptySubtext: {
fontSize: 14,
color: '#687076',
fontFamily: 'AliRegular',
},
// Modal Styles (Retain but refined)
modalContainer: {
flex: 1,
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
modalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
maxHeight: '85%',
minHeight: 500,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 10,
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 24,
paddingTop: 24,
paddingBottom: 16,
},
modalTitle: {
fontSize: 18,
fontWeight: '700',
color: '#1c1f3a',
fontFamily: 'AliBold',
},
modalContent: {
flex: 1,
paddingHorizontal: 24,
},
inputSection: {
backgroundColor: '#F8F9FC',
borderRadius: 24,
padding: 24,
marginBottom: 24,
marginTop: 8,
},
weightInputContainer: {
flexDirection: 'row',
alignItems: 'center',
},
weightIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#EEF0FF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
inputWrapper: {
flex: 1,
flexDirection: 'row',
alignItems: 'baseline',
borderBottomWidth: 2,
borderBottomColor: '#E2E8F0',
paddingBottom: 8,
},
weightDisplay: {
flex: 1,
fontSize: 36,
fontWeight: '700',
textAlign: 'center',
color: '#1c1f3a',
fontFamily: 'AliBold',
},
unitLabel: {
fontSize: 18,
fontWeight: '600',
color: '#6f7ba7',
marginLeft: 8,
fontFamily: 'AliRegular',
},
quickSelectionSection: {
marginBottom: 24,
},
quickSelectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#6f7ba7',
marginBottom: 12,
fontFamily: 'AliRegular',
marginLeft: 4,
},
quickButtons: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
quickButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: '#F1F5F9',
minWidth: 64,
alignItems: 'center',
},
quickButtonSelected: {
backgroundColor: '#4F5BD5',
},
quickButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#64748B',
fontFamily: 'AliRegular',
},
quickButtonTextSelected: {
color: '#FFFFFF',
fontWeight: '700',
},
modalFooter: {
paddingHorizontal: 24,
paddingTop: 16,
paddingBottom: 34,
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
},
saveButton: {
borderRadius: 24,
overflow: 'hidden',
shadowColor: '#4F5BD5',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 8,
},
saveButtonGradient: {
paddingVertical: 16,
alignItems: 'center',
},
saveButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '700',
fontFamily: 'AliBold',
},
});