Files
digital-pilates/app/food-library.tsx
richarjiang bca6670390 Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
2025-11-28 17:29:51 +08:00

981 lines
30 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 { CreateCustomFoodModal, type CustomFoodData } from '@/components/model/food/CreateCustomFoodModal';
import { FoodDetailModal } from '@/components/model/food/FoodDetailModal';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { DEFAULT_IMAGE_FOOD } from '@/constants/Image';
import { useAppDispatch } from '@/hooks/redux';
import { useFoodLibrary, useFoodSearch } from '@/hooks/useFoodLibrary';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { addDietRecord, type CreateDietRecordDto } from '@/services/dietRecords';
import { foodLibraryApi, type CreateCustomFoodDto } from '@/services/foodLibraryApi';
import { fetchDailyNutritionData } from '@/store/nutritionSlice';
import type { FoodItem, MealType, SelectedFoodItem } from '@/types/food';
import { saveNutritionToHealthKit } from '@/utils/health';
import { Ionicons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
Dimensions,
Modal,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width } = Dimensions.get('window');
export default function FoodLibraryScreen() {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop();
const insets = useSafeAreaInsets();
const router = useRouter();
const params = useLocalSearchParams<{ mealType?: string }>();
const mealType = (params.mealType as MealType) || 'breakfast';
// Redux hooks
const dispatch = useAppDispatch();
const { categories, loading, error, clearErrors, loadFoodLibrary } = useFoodLibrary();
const { searchResults, searchLoading, search, clearResults } = useFoodSearch();
// 本地状态
const [selectedCategoryId, setSelectedCategoryId] = useState('common');
const [searchText, setSearchText] = useState('');
const [selectedFood, setSelectedFood] = useState<FoodItem | null>(null);
const [showFoodDetail, setShowFoodDetail] = useState(false);
const [selectedFoodItems, setSelectedFoodItems] = useState<SelectedFoodItem[]>([]);
const [showMealSelector, setShowMealSelector] = useState(false);
const [currentMealType, setCurrentMealType] = useState<MealType>(mealType);
const [isRecording, setIsRecording] = useState(false);
const [showCreateCustomFood, setShowCreateCustomFood] = useState(false);
const getMealTypeLabel = (type: MealType) => {
const labels: Record<MealType, string> = {
breakfast: t('foodLibrary.mealTypes.breakfast'),
lunch: t('foodLibrary.mealTypes.lunch'),
dinner: t('foodLibrary.mealTypes.dinner'),
snack: t('foodLibrary.mealTypes.snack'),
};
return labels[type] || type;
};
// 获取当前选中的分类
const selectedCategory = categories.find(cat => cat.id === selectedCategoryId);
// 过滤食物列表 - 优先显示搜索结果
const filteredFoods = useMemo(() => {
if (searchText.trim() && searchResults.length > 0) {
return searchResults;
}
if (selectedCategory) {
return selectedCategory.foods
}
return [];
}, [searchText, searchResults, selectedCategory]);
// 处理搜索
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchText.trim()) {
search(searchText);
} else {
clearResults();
}
}, 300); // 防抖
return () => clearTimeout(timeoutId);
}, [searchText, search, clearResults]);
// 处理食物选择 - 显示详情弹窗
const handleSelectFood = (food: FoodItem) => {
console.log('选择食物:', food);
setSelectedFood(food);
setShowFoodDetail(true);
console.log('设置弹窗状态:', {
showFoodDetail: true,
selectedFood: food,
foodName: food.name,
foodId: food.id
});
};
// 处理食物保存
const handleSaveFood = (food: FoodItem, amount: number, unit: string) => {
// 计算实际热量
const actualCalories = Math.round((food.calories * amount) / 100);
// 创建新的选择项目
const newSelectedItem: SelectedFoodItem = {
id: `${food.id}_${Date.now()}`, // 使用时间戳确保唯一性
food,
amount,
unit,
calories: actualCalories
};
// 添加到已选择列表
setSelectedFoodItems(prev => [...prev, newSelectedItem]);
console.log('保存食物:', food, amount, unit, '热量:', actualCalories);
setShowFoodDetail(false);
};
// 移除已选择的食物
const handleRemoveSelectedFood = (itemId: string) => {
setSelectedFoodItems(prev => prev.filter(item => item.id !== itemId));
};
// 计算总热量
const totalCalories = selectedFoodItems.reduce((sum, item) => sum + item.calories, 0);
// 关闭详情弹窗
const handleCloseFoodDetail = () => {
setShowFoodDetail(false);
setSelectedFood(null);
};
// 处理删除自定义食物
const handleDeleteFood = async (foodId: string) => {
try {
await foodLibraryApi.deleteCustomFood(Number(foodId));
// 删除成功后重新加载食物库数据
await loadFoodLibrary();
// 关闭弹窗
handleCloseFoodDetail();
} catch (error) {
console.error('删除食物失败:', error);
Alert.alert(
t('foodLibrary.alerts.deleteFailed.title'),
t('foodLibrary.alerts.deleteFailed.message')
);
}
};
// 处理饮食记录
const handleRecordDiet = async () => {
if (selectedFoodItems.length === 0) return;
setIsRecording(true);
try {
// 逐个记录选中的食物
for (const item of selectedFoodItems) {
const dietRecordData: CreateDietRecordDto = {
mealType: currentMealType,
foodName: item.food.name,
foodDescription: item.food.description,
portionDescription: `${item.amount}${item.unit}`,
estimatedCalories: item.calories,
proteinGrams: item.food.protein ? Number(((item.food.protein * item.amount) / 100).toFixed(2)) : undefined,
carbohydrateGrams: item.food.carbohydrate ? Number(((item.food.carbohydrate * item.amount) / 100).toFixed(2)) : undefined,
fatGrams: item.food.fat ? Number(((item.food.fat * item.amount) / 100).toFixed(2)) : undefined,
fiberGrams: item.food.fiber ? Number(((item.food.fiber * item.amount) / 100).toFixed(2)) : undefined,
sugarGrams: item.food.sugar ? Number(((item.food.sugar * item.amount) / 100).toFixed(2)) : undefined,
sodiumMg: item.food.sodium ? Number(((item.food.sodium * item.amount) / 100).toFixed(2)) : undefined,
additionalNutrition: item.food.additionalNutrition,
source: 'manual',
mealTime: new Date().toISOString(),
imageUrl: item.food.imageUrl,
};
// 先保存到后端
await addDietRecord(dietRecordData);
// 然后尝试同步到 HealthKit非阻塞
// 提取蛋白质、脂肪和碳水化合物数据
const { proteinGrams, fatGrams, carbohydrateGrams, mealTime } = dietRecordData;
if (proteinGrams !== undefined || fatGrams !== undefined || carbohydrateGrams !== undefined) {
// 使用 catch 确保 HealthKit 同步失败不影响后端记录
saveNutritionToHealthKit(
{
proteinGrams: proteinGrams || undefined,
fatGrams: fatGrams || undefined,
carbohydrateGrams: carbohydrateGrams || undefined
},
mealTime
).catch(error => {
// HealthKit 同步失败只记录日志,不影响用户体验
console.error('HealthKit 营养数据同步失败(不影响记录):', error);
});
}
}
// 记录成功后,刷新当天的营养数据
const today = new Date();
await dispatch(fetchDailyNutritionData(today));
// 清空选择列表并返回
setSelectedFoodItems([]);
router.back();
} catch (error) {
console.error('记录饮食失败:', error);
// 这里可以显示错误提示
} finally {
setIsRecording(false);
}
};
// 处理餐次选择
const handleMealTypeSelect = (selectedMealType: MealType) => {
setCurrentMealType(selectedMealType);
setShowMealSelector(false);
};
// 处理创建自定义食物
const handleCreateCustomFood = () => {
setShowCreateCustomFood(true);
};
// 处理保存自定义食物
const handleSaveCustomFood = async (customFoodData: CustomFoodData) => {
try {
// 转换数据格式以匹配API要求
const createData: CreateCustomFoodDto = {
name: customFoodData.name,
caloriesPer100g: customFoodData.calories,
proteinPer100g: customFoodData.protein,
carbohydratePer100g: customFoodData.carbohydrate,
fatPer100g: customFoodData.fat,
imageUrl: customFoodData.imageUrl,
};
// 调用API创建自定义食物
const createdFood = await foodLibraryApi.createCustomFood(createData);
// 需要拉取一遍最新的食物列表
await loadFoodLibrary();
// 创建FoodItem对象
const customFoodItem: FoodItem = {
id: createdFood.id.toString(),
name: createdFood.name,
calories: createdFood.caloriesPer100g || 0,
unit: 'g',
description: createdFood.description || `自定义食物 - ${createdFood.name}`,
imageUrl: createdFood.imageUrl,
protein: createdFood.proteinPer100g,
fat: createdFood.fatPer100g,
carbohydrate: createdFood.carbohydratePer100g,
};
// 添加到选择列表中
const newSelectedItem: SelectedFoodItem = {
id: createdFood.id.toString(),
food: customFoodItem,
amount: customFoodData.defaultAmount,
unit: 'g',
calories: Math.round((customFoodItem.calories * customFoodData.defaultAmount) / 100)
};
setSelectedFoodItems(prev => [...prev, newSelectedItem]);
} catch (error) {
console.error('创建自定义食物失败:', error);
Alert.alert(
t('foodLibrary.alerts.createFailed.title'),
t('foodLibrary.alerts.createFailed.message')
);
}
};
// 关闭自定义食物弹窗
const handleCloseCreateCustomFood = () => {
setShowCreateCustomFood(false);
};
// 餐次选择选项
const mealOptions = [
{ key: 'breakfast' as const, label: t('foodLibrary.mealTypes.breakfast'), color: '#FF6B35' },
{ key: 'lunch' as const, label: t('foodLibrary.mealTypes.lunch'), color: '#4CAF50' },
{ key: 'dinner' as const, label: t('foodLibrary.mealTypes.dinner'), color: '#2196F3' },
{ key: 'snack' as const, label: t('foodLibrary.mealTypes.snack'), color: '#FF9800' },
];
return (
<View style={styles.container}>
{/* 头部 */}
<HeaderBar
title={t('foodLibrary.title')}
onBack={() => router.back()}
variant="default"
right={
<TouchableOpacity style={styles.customButton} onPress={handleCreateCustomFood}>
<Ionicons name="add-circle-outline" size={24} color={Colors.light.primary} />
<Text style={styles.customButtonText}>{t('foodLibrary.custom')}</Text>
</TouchableOpacity>
}
/>
<View style={{ height: safeAreaTop }} />
{/* 搜索框 */}
<View style={styles.searchWrapper}>
<View style={styles.searchContainer}>
<Ionicons name="search" size={20} color="#94A3B8" style={styles.searchIcon} />
<TextInput
style={styles.searchInput}
placeholder={t('foodLibrary.search.placeholder')}
value={searchText}
onChangeText={setSearchText}
placeholderTextColor="#94A3B8"
/>
{searchText.length > 0 && (
<TouchableOpacity onPress={() => setSearchText('')}>
<Ionicons name="close-circle" size={18} color="#94A3B8" />
</TouchableOpacity>
)}
</View>
</View>
{/* 主要内容区域 - Split View Card */}
<View style={styles.mainContentCard}>
{loading && categories.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={Colors.light.primary} />
<Text style={styles.loadingText}>{t('foodLibrary.loading')}</Text>
</View>
) : error ? (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={() => {
clearErrors();
// 这里可以重新加载数据
}}
>
<Text style={styles.retryButtonText}>{t('foodLibrary.retry')}</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.splitViewContainer}>
{/* 左侧分类导航 */}
<View style={styles.categorySidebar}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.categoryListContent}>
{categories.map((category) => (
<TouchableOpacity
key={category.id}
style={[
styles.categoryItem,
selectedCategoryId === category.id && styles.categoryItemActive
]}
onPress={() => {
setSelectedCategoryId(category.id);
if (searchText) {
setSearchText('');
clearResults();
}
}}
>
<View style={[
styles.categoryIndicator,
selectedCategoryId === category.id && styles.categoryIndicatorActive
]} />
<Text style={[
styles.categoryText,
selectedCategoryId === category.id && styles.categoryTextActive
]}>
{category.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
{/* 右侧食物列表 */}
<View style={styles.foodListContainer}>
{searchLoading ? (
<View style={styles.searchLoadingContainer}>
<ActivityIndicator size="small" color={Colors.light.primary} />
<Text style={styles.searchLoadingText}>{t('foodLibrary.search.loading')}</Text>
</View>
) : (
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.foodListContent}
>
{filteredFoods.map((food) => (
<TouchableOpacity
key={food.id}
style={styles.foodItemCard}
onPress={() => handleSelectFood(food)}
activeOpacity={0.7}
>
<Image
style={styles.foodImage}
source={{ uri: food.imageUrl || DEFAULT_IMAGE_FOOD }}
cachePolicy={'memory-disk'}
/>
<View style={styles.foodInfo}>
<Text style={styles.foodName} numberOfLines={1}>{food.name}</Text>
<Text style={styles.foodCalories}>
{food.calories} <Text style={styles.unitText}>kcal/{food.unit}</Text>
</Text>
</View>
<View style={styles.addButton}>
<Ionicons name="add" size={16} color="#FFF" />
</View>
</TouchableOpacity>
))}
{filteredFoods.length === 0 && !searchLoading && (
<View style={styles.emptyContainer}>
<Image
source={require('@/assets/images/task/ImageEmpty.png')}
style={{ width: 80, height: 80, opacity: 0.5, marginBottom: 10 }}
/>
<Text style={styles.emptyText}>
{searchText ? t('foodLibrary.search.empty') : t('foodLibrary.search.noData')}
</Text>
</View>
)}
{/* 底部留白,防止被底部栏遮挡 */}
<View style={{ height: 100 }} />
</ScrollView>
)}
</View>
</View>
)}
</View>
{/* 底部操作栏 - 悬浮样式 */}
<View style={[styles.bottomBarContainer, { paddingBottom: Math.max(insets.bottom, 20) }]}>
{/* 已选择食物概览 (如果有选择) */}
{selectedFoodItems.length > 0 && (
<View style={styles.selectedPreviewContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.selectedList}>
{selectedFoodItems.map((item) => (
<TouchableOpacity
key={item.id}
style={styles.selectedChip}
onPress={() => handleRemoveSelectedFood(item.id)}
>
<Image
source={{ uri: item.food.imageUrl || DEFAULT_IMAGE_FOOD }}
style={styles.selectedChipImage}
/>
<Text style={styles.selectedChipText}>{item.amount}{item.unit}</Text>
<View style={styles.selectedChipClose}>
<Ionicons name="close" size={10} color="#FFF" />
</View>
</TouchableOpacity>
))}
</ScrollView>
<View style={styles.totalCaloriesBadge}>
<Text style={styles.totalCaloriesText}>{totalCalories} kcal</Text>
</View>
</View>
)}
<View style={styles.actionRow}>
<TouchableOpacity
style={styles.mealSelectorButton}
onPress={() => setShowMealSelector(true)}
>
<View style={[
styles.mealDot,
{ backgroundColor: mealOptions.find(option => option.key === currentMealType)?.color || '#FF6B35' }
]} />
<Text style={styles.mealSelectorText}>{getMealTypeLabel(currentMealType)}</Text>
<Ionicons name="chevron-down" size={16} color="#1c1f3a" />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.confirmButton,
(selectedFoodItems.length === 0 || isRecording) && styles.confirmButtonDisabled
]}
disabled={selectedFoodItems.length === 0 || isRecording}
onPress={handleRecordDiet}
>
{isRecording ? (
<ActivityIndicator size="small" color="#FFF" />
) : (
<>
<Text style={styles.confirmButtonText}>{t('foodLibrary.actions.record')} ({selectedFoodItems.length})</Text>
<Ionicons name="arrow-forward" size={18} color="#FFF" />
</>
)}
</TouchableOpacity>
</View>
</View>
{/* 餐次选择弹窗 */}
<Modal
visible={showMealSelector}
transparent={true}
animationType="fade"
onRequestClose={() => setShowMealSelector(false)}
>
<View style={styles.modalOverlay}>
<TouchableOpacity
style={styles.modalBackdrop}
onPress={() => setShowMealSelector(false)}
/>
<View style={[styles.modalContent, { paddingBottom: insets.bottom + 20 }]}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>{t('foodLibrary.actions.selectMeal')}</Text>
<TouchableOpacity onPress={() => setShowMealSelector(false)}>
<Ionicons name="close-circle" size={24} color="#94A3B8" />
</TouchableOpacity>
</View>
<View style={styles.mealOptionsList}>
{mealOptions.map((option) => (
<TouchableOpacity
key={option.key}
style={[
styles.mealOptionItem,
currentMealType === option.key && styles.mealOptionItemActive
]}
onPress={() => handleMealTypeSelect(option.key)}
>
<View style={[styles.mealOptionDot, { backgroundColor: option.color }]} />
<Text style={[
styles.mealOptionLabel,
currentMealType === option.key && styles.mealOptionLabelActive
]}>
{option.label}
</Text>
{currentMealType === option.key && (
<Ionicons name="checkmark-circle" size={20} color={Colors.light.primary} />
)}
</TouchableOpacity>
))}
</View>
</View>
</View>
</Modal>
{/* 食物详情弹窗 */}
<FoodDetailModal
visible={showFoodDetail}
food={selectedFood}
category={selectedCategory}
onClose={handleCloseFoodDetail}
onSave={handleSaveFood}
onDelete={handleDeleteFood}
/>
{/* 创建自定义食物弹窗 */}
<CreateCustomFoodModal
visible={showCreateCustomFood}
onClose={handleCloseCreateCustomFood}
onSave={handleSaveCustomFood}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f3f4fb', // Matches sleep-detail background
},
customButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
padding: 4,
},
customButtonText: {
fontSize: 14,
color: Colors.light.primary,
fontWeight: '600',
fontFamily: 'AliBold',
},
// Search Area
searchWrapper: {
paddingHorizontal: 20,
paddingBottom: 16,
zIndex: 10,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 20,
shadowColor: 'rgba(30, 41, 59, 0.05)',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 1,
shadowRadius: 12,
elevation: 4,
},
searchIcon: {
marginRight: 10,
},
searchInput: {
flex: 1,
fontSize: 15,
color: '#1c1f3a',
fontFamily: 'AliRegular',
padding: 0, // Remove Android default padding
},
// Main Content Card
mainContentCard: {
flex: 1,
marginHorizontal: 20,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
shadowColor: 'rgba(30, 41, 59, 0.08)',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 1,
shadowRadius: 16,
elevation: 5,
overflow: 'hidden',
},
splitViewContainer: {
flex: 1,
flexDirection: 'row',
},
// Sidebar
categorySidebar: {
width: 90,
backgroundColor: '#F8FAFC',
borderRightWidth: 1,
borderRightColor: '#F1F5F9',
},
categoryListContent: {
paddingVertical: 12,
},
categoryItem: {
paddingVertical: 16,
paddingHorizontal: 8,
alignItems: 'center',
position: 'relative',
},
categoryItemActive: {
backgroundColor: '#FFFFFF',
},
categoryIndicator: {
position: 'absolute',
left: 0,
top: 12,
bottom: 12,
width: 3,
borderTopRightRadius: 3,
borderBottomRightRadius: 3,
backgroundColor: 'transparent',
},
categoryIndicatorActive: {
backgroundColor: Colors.light.primary,
},
categoryText: {
fontSize: 13,
color: '#64748B',
textAlign: 'center',
fontFamily: 'AliRegular',
},
categoryTextActive: {
color: Colors.light.primary,
fontFamily: 'AliBold',
fontWeight: '600',
},
// Food List
foodListContainer: {
flex: 1,
backgroundColor: '#FFFFFF',
},
foodListContent: {
padding: 16,
},
foodItemCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
marginBottom: 12,
backgroundColor: '#FFFFFF',
borderRadius: 16,
borderWidth: 1,
borderColor: '#F1F5F9',
shadowColor: 'rgba(148, 163, 184, 0.1)',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 1,
shadowRadius: 6,
elevation: 2,
},
foodImage: {
width: 48,
height: 48,
borderRadius: 12,
marginRight: 12,
backgroundColor: '#F8FAFC',
},
foodInfo: {
flex: 1,
marginRight: 8,
},
foodName: {
fontSize: 15,
color: '#1E293B',
fontFamily: 'AliBold',
marginBottom: 4,
},
foodCalories: {
fontSize: 13,
color: Colors.light.primary,
fontFamily: 'AliBold',
},
unitText: {
fontSize: 11,
color: '#94A3B8',
fontFamily: 'AliRegular',
fontWeight: 'normal',
},
addButton: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: Colors.light.primary,
alignItems: 'center',
justifyContent: 'center',
},
// Empty States
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#94A3B8',
fontFamily: 'AliRegular',
},
errorContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
errorText: {
fontSize: 14,
color: '#EF4444',
textAlign: 'center',
marginBottom: 16,
fontFamily: 'AliRegular',
},
retryButton: {
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: Colors.light.primary,
borderRadius: 20,
},
retryButtonText: {
color: '#FFF',
fontSize: 14,
fontFamily: 'AliBold',
},
searchLoadingContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
searchLoadingText: {
marginLeft: 8,
fontSize: 14,
color: '#94A3B8',
fontFamily: 'AliRegular',
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 60,
},
emptyText: {
fontSize: 14,
color: '#94A3B8',
fontFamily: 'AliRegular',
},
// Bottom Bar
bottomBarContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
paddingHorizontal: 20,
shadowColor: 'rgba(30, 41, 59, 0.1)',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 1,
shadowRadius: 16,
elevation: 8,
},
selectedPreviewContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
selectedList: {
flexGrow: 0,
},
selectedChip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F1F5F9',
borderRadius: 16,
paddingLeft: 4,
paddingRight: 8,
paddingVertical: 4,
marginRight: 8,
},
selectedChipImage: {
width: 24,
height: 24,
borderRadius: 12,
marginRight: 6,
},
selectedChipText: {
fontSize: 12,
color: '#475569',
fontFamily: 'AliBold',
marginRight: 6,
},
selectedChipClose: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#CBD5E1',
alignItems: 'center',
justifyContent: 'center',
},
totalCaloriesBadge: {
marginLeft: 'auto',
backgroundColor: '#EEF2FF',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
totalCaloriesText: {
fontSize: 12,
color: Colors.light.primary,
fontFamily: 'AliBold',
},
actionRow: {
flexDirection: 'row',
gap: 12,
},
mealSelectorButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F8FAFC',
borderRadius: 24,
height: 48,
borderWidth: 1,
borderColor: '#E2E8F0',
},
mealDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 8,
},
mealSelectorText: {
fontSize: 15,
color: '#1E293B',
fontFamily: 'AliBold',
marginRight: 6,
},
confirmButton: {
flex: 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Colors.light.primary,
borderRadius: 24,
height: 48,
shadowColor: Colors.light.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
gap: 8,
},
confirmButtonDisabled: {
backgroundColor: '#94A3B8',
shadowOpacity: 0,
},
confirmButtonText: {
fontSize: 16,
color: '#FFF',
fontFamily: 'AliBold',
fontWeight: '600',
},
// Modal Styles
modalOverlay: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(15, 23, 42, 0.4)',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
},
modalContent: {
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
padding: 24,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
modalTitle: {
fontSize: 20,
color: '#1E293B',
fontFamily: 'AliBold',
},
mealOptionsList: {
gap: 12,
},
mealOptionItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 16,
backgroundColor: '#F8FAFC',
borderWidth: 1,
borderColor: '#F1F5F9',
},
mealOptionItemActive: {
backgroundColor: '#EEF2FF',
borderColor: Colors.light.primary,
},
mealOptionDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 16,
},
mealOptionLabel: {
flex: 1,
fontSize: 16,
color: '#475569',
fontFamily: 'AliRegular',
},
mealOptionLabelActive: {
color: '#1E293B',
fontFamily: 'AliBold',
},
});