scan food
This commit is contained in:
parent
ca64a100b6
commit
21afb085e3
Binary file not shown.
@ -5,6 +5,7 @@ import {
|
|||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@ -244,166 +245,184 @@ export function ScanFoodModal({
|
|||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
style={styles.resultContainer}
|
style={styles.resultContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.resultHeader}>
|
<View style={styles.resultSheet}>
|
||||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
<View style={styles.resultHeader}>
|
||||||
<Ionicons name="close" size={28} color={theme.colors.gray900} />
|
<TouchableOpacity
|
||||||
</TouchableOpacity>
|
onPress={handleReset}
|
||||||
<Text style={styles.resultTitle}>
|
style={styles.closeButton}
|
||||||
{notFound ? "Barcode Not Found" : "Food Details"}
|
>
|
||||||
</Text>
|
<Ionicons
|
||||||
<View style={{ width: 28 }} />
|
name="scan"
|
||||||
</View>
|
size={24}
|
||||||
|
color={theme.colors.gray700}
|
||||||
<View style={styles.foodCard}>
|
|
||||||
{!notFound && foodData ? (
|
|
||||||
<>
|
|
||||||
<View style={styles.foodIconContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.success}
|
|
||||||
style={styles.foodIcon}
|
|
||||||
>
|
|
||||||
<Ionicons name="restaurant" size={32} color="#fff" />
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.foodName}>{foodData.name}</Text>
|
|
||||||
{foodData.brand ? (
|
|
||||||
<Text style={styles.servingSize}>{foodData.brand}</Text>
|
|
||||||
) : null}
|
|
||||||
<Text style={styles.servingSize}>{foodData.servingSize}</Text>
|
|
||||||
|
|
||||||
<View style={styles.caloriesBadge}>
|
|
||||||
<Text style={styles.caloriesValue}>
|
|
||||||
{foodData.caloriesPerServing}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.caloriesLabel}>kcal per serving</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.servingsContainer}>
|
|
||||||
<Text style={styles.label}>Number of Servings</Text>
|
|
||||||
<View style={styles.servingsInput}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() =>
|
|
||||||
setServings(
|
|
||||||
String(
|
|
||||||
Math.max(0.5, parseFloat(servings || "1") - 0.5),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
style={styles.servingsButton}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name="remove"
|
|
||||||
size={20}
|
|
||||||
color={theme.colors.primary}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TextInput
|
|
||||||
style={styles.servingsValue}
|
|
||||||
value={servings}
|
|
||||||
onChangeText={setServings}
|
|
||||||
keyboardType="decimal-pad"
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() =>
|
|
||||||
setServings(String(parseFloat(servings || "1") + 0.5))
|
|
||||||
}
|
|
||||||
style={styles.servingsButton}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name="add"
|
|
||||||
size={20}
|
|
||||||
color={theme.colors.primary}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Text style={styles.servingSize}>
|
|
||||||
We could not find this product in OpenFoodFacts.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Food Name</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={manualFoodName}
|
|
||||||
onChangeText={setManualFoodName}
|
|
||||||
placeholder="Enter food name"
|
|
||||||
placeholderTextColor={theme.colors.gray400}
|
|
||||||
/>
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
<Text style={styles.label}>Calories</Text>
|
<Text style={styles.resultTitle}>
|
||||||
<View style={styles.caloriesInputContainer}>
|
{notFound ? "Barcode Not Found" : "Food Details"}
|
||||||
<TextInput
|
</Text>
|
||||||
style={[styles.input, styles.caloriesInput]}
|
<View style={{ width: 24 }} />
|
||||||
value={manualCalories}
|
|
||||||
onChangeText={setManualCalories}
|
|
||||||
keyboardType="number-pad"
|
|
||||||
placeholder="0"
|
|
||||||
placeholderTextColor={theme.colors.gray400}
|
|
||||||
/>
|
|
||||||
<Text style={styles.caloriesUnit}>kcal</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.mealTypeContainer}>
|
|
||||||
<Text style={styles.label}>Meal Type</Text>
|
|
||||||
<View style={styles.mealTypeRow}>
|
|
||||||
{MEAL_TYPES.map((type) => {
|
|
||||||
const active = mealType === type;
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={type}
|
|
||||||
onPress={() => setMealType(type)}
|
|
||||||
style={[
|
|
||||||
styles.mealTypeChip,
|
|
||||||
active && styles.mealTypeChipActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.mealTypeText,
|
|
||||||
active && styles.mealTypeTextActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.totalCalories}>
|
<ScrollView
|
||||||
|
style={styles.resultScroll}
|
||||||
|
contentContainerStyle={styles.resultScrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={styles.foodCard}>
|
||||||
|
{!notFound && foodData ? (
|
||||||
|
<>
|
||||||
|
<View style={styles.foodIconContainer}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={theme.gradients.success}
|
||||||
|
style={styles.foodIcon}
|
||||||
|
>
|
||||||
|
<Ionicons name="restaurant" size={28} color="#fff" />
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.foodName}>{foodData.name}</Text>
|
||||||
|
<Text style={styles.servingMeta}>
|
||||||
|
{[foodData.brand, foodData.servingSize]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" • ")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.caloriesBadge}>
|
||||||
|
<Text style={styles.caloriesValue}>
|
||||||
|
{foodData.caloriesPerServing}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.caloriesLabel}>
|
||||||
|
kcal per serving
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.servingsContainer}>
|
||||||
|
<Text style={styles.label}>Servings</Text>
|
||||||
|
<View style={styles.servingsInput}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() =>
|
||||||
|
setServings(
|
||||||
|
String(
|
||||||
|
Math.max(
|
||||||
|
0.5,
|
||||||
|
parseFloat(servings || "1") - 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={styles.servingsButton}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="remove"
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TextInput
|
||||||
|
style={styles.servingsValue}
|
||||||
|
value={servings}
|
||||||
|
onChangeText={setServings}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() =>
|
||||||
|
setServings(
|
||||||
|
String(parseFloat(servings || "1") + 0.5),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={styles.servingsButton}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="add"
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text style={styles.servingMeta}>
|
||||||
|
We could not find this product in OpenFoodFacts.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Food Name</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={manualFoodName}
|
||||||
|
onChangeText={setManualFoodName}
|
||||||
|
placeholder="Enter food name"
|
||||||
|
placeholderTextColor={theme.colors.gray400}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Calories</Text>
|
||||||
|
<View style={styles.caloriesInputContainer}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, styles.caloriesInput]}
|
||||||
|
value={manualCalories}
|
||||||
|
onChangeText={setManualCalories}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
placeholder="0"
|
||||||
|
placeholderTextColor={theme.colors.gray400}
|
||||||
|
/>
|
||||||
|
<Text style={styles.caloriesUnit}>kcal</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.mealTypeContainer}>
|
||||||
|
<Text style={styles.label}>Meal Type</Text>
|
||||||
|
<View style={styles.mealTypeRow}>
|
||||||
|
{MEAL_TYPES.map((type) => {
|
||||||
|
const active = mealType === type;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={type}
|
||||||
|
onPress={() => setMealType(type)}
|
||||||
|
style={[
|
||||||
|
styles.mealTypeChip,
|
||||||
|
active && styles.mealTypeChipActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.mealTypeText,
|
||||||
|
active && styles.mealTypeTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View style={styles.footerSummary}>
|
||||||
<Text style={styles.totalLabel}>Total Calories</Text>
|
<Text style={styles.totalLabel}>Total Calories</Text>
|
||||||
<Text style={styles.totalValue}>{getTotalCalories()} kcal</Text>
|
<Text style={styles.totalValue}>{getTotalCalories()} kcal</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity onPress={onClose} style={styles.rejectButton}>
|
||||||
onPress={handleReset}
|
<Text style={styles.rejectButtonText}>Reject</Text>
|
||||||
style={styles.rescanButton}
|
</TouchableOpacity>
|
||||||
>
|
|
||||||
<Text style={styles.rescanButtonText}>Scan Again</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleConfirm}
|
onPress={handleConfirm}
|
||||||
style={styles.confirmButtonContainer}
|
style={styles.confirmButtonContainer}
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={theme.gradients.success}
|
|
||||||
style={styles.confirmButton}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.confirmButtonText}>
|
<LinearGradient
|
||||||
{notFound ? "Add Manual" : "Add to Diary"}
|
colors={theme.gradients.success}
|
||||||
</Text>
|
style={styles.confirmButton}
|
||||||
</LinearGradient>
|
>
|
||||||
</TouchableOpacity>
|
<Text style={styles.confirmButtonText}>Add to Count</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
)}
|
)}
|
||||||
@ -463,15 +482,32 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
resultContainer: {
|
resultContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.35)",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
resultSheet: {
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
maxHeight: "80%",
|
||||||
},
|
},
|
||||||
resultHeader: {
|
resultHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: 20,
|
paddingHorizontal: 20,
|
||||||
paddingTop: 60,
|
paddingVertical: 14,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: theme.colors.gray100,
|
||||||
|
},
|
||||||
|
resultScroll: {
|
||||||
|
flexGrow: 0,
|
||||||
|
},
|
||||||
|
resultScrollContent: {
|
||||||
|
paddingBottom: 12,
|
||||||
},
|
},
|
||||||
resultTitle: {
|
resultTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@ -479,46 +515,47 @@ const styles = StyleSheet.create({
|
|||||||
color: theme.colors.gray900,
|
color: theme.colors.gray900,
|
||||||
},
|
},
|
||||||
foodCard: {
|
foodCard: {
|
||||||
margin: 20,
|
marginHorizontal: 16,
|
||||||
padding: 24,
|
marginTop: 12,
|
||||||
|
padding: 16,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
borderRadius: 24,
|
borderRadius: 16,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
...theme.shadows.medium,
|
...theme.shadows.medium,
|
||||||
},
|
},
|
||||||
foodIconContainer: {
|
foodIconContainer: {
|
||||||
marginBottom: 16,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
foodIcon: {
|
foodIcon: {
|
||||||
width: 80,
|
width: 64,
|
||||||
height: 80,
|
height: 64,
|
||||||
borderRadius: 40,
|
borderRadius: 32,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
foodName: {
|
foodName: {
|
||||||
fontSize: 24,
|
fontSize: 20,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: theme.colors.gray900,
|
color: theme.colors.gray900,
|
||||||
marginBottom: 8,
|
marginBottom: 4,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
servingSize: {
|
servingMeta: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
color: theme.colors.gray600,
|
color: theme.colors.gray600,
|
||||||
marginBottom: 16,
|
marginBottom: 12,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
caloriesBadge: {
|
caloriesBadge: {
|
||||||
backgroundColor: theme.colors.gray50,
|
backgroundColor: theme.colors.gray50,
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 18,
|
||||||
paddingVertical: 16,
|
paddingVertical: 10,
|
||||||
borderRadius: 16,
|
borderRadius: 12,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: 24,
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
caloriesValue: {
|
caloriesValue: {
|
||||||
fontSize: 32,
|
fontSize: 26,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: theme.colors.primary,
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
@ -529,11 +566,11 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
servingsContainer: {
|
servingsContainer: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
marginBottom: 24,
|
marginBottom: 14,
|
||||||
},
|
},
|
||||||
mealTypeContainer: {
|
mealTypeContainer: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
marginBottom: 24,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
mealTypeRow: {
|
mealTypeRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@ -615,13 +652,18 @@ const styles = StyleSheet.create({
|
|||||||
color: theme.colors.gray500,
|
color: theme.colors.gray500,
|
||||||
},
|
},
|
||||||
totalCalories: {
|
totalCalories: {
|
||||||
width: "100%",
|
display: "none",
|
||||||
|
},
|
||||||
|
footerSummary: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingTop: 24,
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: theme.colors.gray200,
|
borderTopColor: theme.colors.gray200,
|
||||||
|
backgroundColor: "#fff",
|
||||||
},
|
},
|
||||||
totalLabel: {
|
totalLabel: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@ -635,17 +677,20 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
padding: 20,
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingBottom: 16,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
|
backgroundColor: "#fff",
|
||||||
},
|
},
|
||||||
rescanButton: {
|
rejectButton: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: theme.colors.gray100,
|
backgroundColor: theme.colors.gray100,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
rescanButtonText: {
|
rejectButtonText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: theme.colors.gray700,
|
color: theme.colors.gray700,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user