- 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.
981 lines
30 KiB
TypeScript
981 lines
30 KiB
TypeScript
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',
|
||
},
|
||
}); |