Integrate Food Editor with Recipe
The API below is used for food search.
/**
* Look up the food item result for a given Passio ID.
* @param passioID - The Passio ID for the query.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchFoodItemForPassioID(passioID: PassioID): Promise<PassioFoodItem | null>
/**
* Look up the food item result for a given refCode.
* @param refCode - The refCode for the query.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchFoodItemForRefCode(refCode: RefCode): Promise<PassioFoodItem | null>
/**
* Look up the food item result for a given by barcode or packagedFoodCode.
* @param barcode - barcode for the query.
* or
* @param packageFoodCode - packageFoodCode for the query.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchFoodItemForProductCode(
code: Barcode | PackagedFoodCode
): Promise<PassioFoodItem | null>
Example
useFoodDetail
import { useCallback, useEffect, useState } from 'react'
import {
PassioSDK,
type PassioFoodItem,
type PassioNutrients,
type ServingUnit,
ingredientWeightInGram,
selectedServingUnitGram,
} from '@passiolife/nutritionai-react-native-sdk-v3'
interface Props {
passioFoodItem: PassioFoodItem
}
export interface FoodNutrient {
title: string
value: number
unit: string
}
export interface ComputedWeight {
value: number
qty: string
}
const modifyPassioFoodItemByCheckSelectedUnitExist = (
passioFoodItem: PassioFoodItem
) => {
const updatedFoodItem = passioFoodItem
const item = updatedFoodItem.amount.servingUnits?.find(
(i) => i.unitName === updatedFoodItem?.amount.selectedUnit
)
if (item === undefined) {
updatedFoodItem.amount.selectedUnit = 'gram'
updatedFoodItem.amount.selectedQuantity =
updatedFoodItem.amount?.weight.value
}
return updatedFoodItem
}
export const useFoodDetail = (prop: Props) => {
const [passioFoodItem, setPassioFoodItem] = useState(
modifyPassioFoodItemByCheckSelectedUnitExist({ ...prop.passioFoodItem })
)
const [isAddIngredients, openAddIngredients] = useState<boolean>(false)
const [foodNutrients, setFoodNutrients] = useState<FoodNutrient[]>([])
const [textInput, setTextInput] = useState(
prop.passioFoodItem.amount.selectedQuantity.toString() ?? '1'
)
useEffect(() => {
function init() {
setFoodNutrients(
extractFoodNutrients(
PassioSDK.getNutrientsOfPassioFoodItem(
passioFoodItem,
passioFoodItem.amount.weight
)
)
)
}
init()
}, [passioFoodItem])
const onServingQuantityChange = useCallback(
(value: string) => {
setTextInput(value)
console.log(passioFoodItem.amount.selectedUnit)
const newQuantity = Number(value.length > 0 ? value : 1)
const weight =
passioFoodItem.amount.servingUnits?.filter(
(i) => i.unitName === passioFoodItem.amount.selectedUnit
)[0].value ?? 1
setPassioFoodItem((foodItem) => {
foodItem.amount.weight.value = weight * newQuantity
foodItem.amount.selectedQuantity = newQuantity
return {
...foodItem,
}
})
},
[passioFoodItem.amount.selectedUnit, passioFoodItem.amount.servingUnits]
)
const onServingSizeSelect = useCallback(
(value: ServingUnit) => {
const defaultWeight = passioFoodItem.amount.weight.value ?? 0
const newQuantity = Number((defaultWeight / value.value).toFixed(2))
setTextInput((newQuantity ?? 1).toString())
setPassioFoodItem((foodItem) => {
foodItem.amount.selectedUnit = value.unitName
foodItem.amount.selectedQuantity = newQuantity
return {
...foodItem,
}
})
},
[passioFoodItem.amount.weight.value]
)
function extractFoodNutrients(
passioNutrients: PassioNutrients | null
): FoodNutrient[] {
if (passioNutrients == null) {
return []
}
return Object.entries(passioNutrients)
.map(([title, { value, unit }]) => ({
title,
value: value,
unit,
}))
.filter((item) => item.title !== 'weight')
}
const showAddIngredients = () => {
openAddIngredients(true)
}
const closeAddIngredients = () => {
openAddIngredients(false)
}
const onAddIngredients = (item: PassioFoodItem) => {
closeAddIngredients()
setPassioFoodItem((foodItem) => {
const oldIngredients = foodItem.ingredients ?? []
const newIngredients = item.ingredients ?? []
const ingredients = [...oldIngredients, ...newIngredients]
foodItem.amount.selectedUnit = 'gram'
foodItem.amount.weight.value = ingredientWeightInGram(ingredients)
const newQuantity = Number(
(
(foodItem.amount.weight.value ?? 0) /
selectedServingUnitGram(
'gram',
passioFoodItem.amount.servingUnits ?? []
)
).toFixed(2)
)
foodItem.amount.selectedQuantity = newQuantity
;(foodItem.name =
'Recipe with ' + foodItem.name.replace('Recipe with ', '')),
setTextInput(newQuantity.toString())
foodItem.ingredients = ingredients
return { ...foodItem }
})
}
return {
calculatedWeight: passioFoodItem.amount?.weight.value,
calculatedWeightUnit: passioFoodItem?.amount?.weight?.unit,
selectedServingUnit: passioFoodItem?.amount.selectedUnit,
textInputServingQty: textInput,
extractFoodNutrients,
isAddIngredients,
onServingQuantityChange,
onAddIngredients,
onServingSizeSelect,
showAddIngredients,
closeAddIngredients,
passioFoodItem,
foodNutrients,
}
}
FoodDetail
import React from 'react'
import {
FlatList,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
Image,
View,
Pressable,
Linking,
Modal,
} from 'react-native'
import { useFoodDetail } from './useFoodDetail'
import {
PassioIconView,
IconSize,
PassioFoodItem,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import { } from '../search'
interface Props {
onClose: () => void
passioFoodItem: PassioFoodItem
}
const FoodDetail = (props: Props) => {
const { onClose } = props
const styles = foodDetailStyle()
const {
calculatedWeight,
calculatedWeightUnit,
selectedServingUnit,
onServingQuantityChange,
onServingSizeSelect,
textInputServingQty,
foodNutrients,
showAddIngredients,
onAddIngredients,
passioFoodItem,
closeAddIngredients,
isAddIngredients,
} = useFoodDetail(props)
const nutrients = foodNutrients
const renderNutrientItem = () => (
<View style={styles.nutrientContainer}>
<Text style={styles.title}>Nutrients</Text>
<FlatList
data={nutrients.filter((item) => item.value >= 1)}
renderItem={({ item }) => {
return (
<View style={styles.nutrientsContainer}>
<Text style={styles.nutrientTitle}>
{getNutrientName[item.title] ?? item.title}
</Text>
<Text>
{Math.round(item.value)}
{' ' + item.unit}
</Text>
</View>
)
}}
/>
</View>
)
const renderEditServing = () => {
return (
<View style={styles.nutrientContainer}>
<Text style={styles.title}>{`Serving Sizes (${Math.round(
calculatedWeight ?? 0
)} ${calculatedWeightUnit})`}</Text>
<TextInput
value={textInputServingQty.toString()}
style={styles.textInput}
keyboardType="numeric"
placeholder="Serving Size"
placeholderTextColor={'gray'}
onChangeText={onServingQuantityChange}
/>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={passioFoodItem.amount.servingUnits}
renderItem={({ item }) => (
<Pressable
onPress={() => onServingSizeSelect(item)}
style={[
styles.servingContainer,
selectedServingUnit === item.unitName &&
styles.servingSelectedContainer,
]}
>
<Text
style={[
styles.servingContainerTitle,
selectedServingUnit === item.unitName &&
styles.servingSelectedContainerTitle,
]}
>
{item.unitName}
</Text>
</Pressable>
)}
/>
</View>
)
}
const renderIngredient = () => {
return (
<View style={styles.nutrientContainer}>
{passioFoodItem.ingredients &&
passioFoodItem.ingredients.length > 1 && (
<>
<Text style={styles.title}>Ingredients</Text>
<FlatList
data={passioFoodItem.ingredients}
keyExtractor={(item, index) => item.toString() + index}
renderItem={({ item }) => (
<View style={styles.ingredientsContainer}>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: item.iconId,
iconSize: IconSize.PX360,
}}
/>
</View>
<View style={styles.ingredientDetailContainer}>
<Text style={styles.ingredientTitle}>{item.name}</Text>
<Text style={styles.ingredientDetail}>
{item.amount?.selectedUnit}{' '}
</Text>
</View>
</View>
)}
/>
</>
)}
<Pressable
style={styles.addIngredientsContainer}
onPress={showAddIngredients}
>
<Text style={styles.addIngredients}>Add Ingredients</Text>
</Pressable>
</View>
)
}
const renderMacro = () => {
return (
<View style={styles.macroContainer}>
{['Calories', 'Carbs', 'Protein', 'Fat'].map((title) => {
const nutrient = nutrients.find(
(item) => item.title.toLowerCase() === title.toLowerCase()
)
return (
<View key={title} style={styles.macroTitleContainer}>
<Text style={styles.macroTitle}>{title}</Text>
<Text>{(nutrient?.value ?? 0).toFixed(2)}</Text>
</View>
)
})}
</View>
)
}
const renderOpenFood = () => {
const openLink = (url: string) => {
Linking.openURL(url)
}
return (
<Text style={styles.openFood}>
This nutrition information provided can be found from{' '}
<Text
style={styles.link}
onPress={() => openLink('https://en.openfoodfacts.org/')}
>
Open Food Facts
</Text>
, which is made available under the{' '}
<Text
style={styles.link}
onPress={() =>
openLink('https://opendatacommons.org/licenses/dbcl/1.0/')
}
>
Open Database License
</Text>
</Text>
)
}
const renderFoodDetailCard = () => {
return (
<View style={styles.itemFoodInfoContainer}>
<View style={styles.itemFoodInfo}>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: passioFoodItem.iconId,
iconSize: IconSize.PX90,
}}
/>
</View>
<View style={styles.flex1}>
<Text style={styles.foodName}>{passioFoodItem.name}</Text>
</View>
</View>
{renderMacro()}
{passioFoodItem.isOpenFood && renderOpenFood()}
</View>
)
}
return (
<SafeAreaView style={styles.container}>
<ScrollView>
<View style={styles.closeButton}>
<TouchableOpacity onPress={onClose}>
<Image
source={require('../assets/back.png')}
style={styles.backIcon}
/>
</TouchableOpacity>
</View>
{passioFoodItem && (
<View style={styles.itemDetailContainer}>
{renderFoodDetailCard()}
<View style={styles.line} />
{renderEditServing()}
<View style={styles.line} />
{renderNutrientItem()}
<View style={styles.line} />
{renderIngredient()}
<View style={styles.line} />
<View style={styles.line} />
<View style={styles.line} />
<View style={styles.line} />
</View>
)}
</ScrollView>
<Modal visible={isAddIngredients}>
<FoodSearchView
onClose={closeAddIngredients}
onFoodDetail={onAddIngredients}
/>
</Modal>
</SafeAreaView>
)
}
const foodDetailStyle = () =>
StyleSheet.create({
addIngredientsContainer: {
paddingVertical: 16,
paddingHorizontal: 16,
justifyContent: 'center',
alignContent: 'center',
alignSelf: 'center',
alignItems: 'center',
marginVertical: 16,
backgroundColor: 'rgba(242, 245, 251, 1)',
},
addIngredients: {
justifyContent: 'center',
alignContent: 'center',
fontSize: 16,
fontWeight: '600',
alignSelf: 'center',
alignItems: 'center',
},
container: {
backgroundColor: 'rgba(242, 245, 251, 1)',
flex: 1,
},
macroTitleContainer: {
flex: 1,
marginTop: 12,
},
ingredientDetailContainer: {
alignSelf: 'center',
},
flex1: {
flex: 1,
},
link: {
color: 'blue',
textDecorationLine: 'underline',
},
macroTitle: {
fontSize: 16,
fontWeight: '600',
},
macroContainer: {
flexDirection: 'row',
flex: 1,
},
closeButton: {
margin: 16,
},
itemFoodInfo: {
flexDirection: 'row',
alignSelf: 'center',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
},
itemFoodInfoContainer: {
borderRadius: 16,
backgroundColor: 'white',
padding: 16,
},
foodName: {
paddingHorizontal: 16,
fontSize: 16,
textTransform: 'capitalize',
fontWeight: '600',
},
foodDetail: {
paddingHorizontal: 16,
},
openFood: {
paddingVertical: 16,
},
nutrientContainer: {
backgroundColor: 'white',
padding: 16,
},
backIcon: {
height: 24,
width: 24,
},
nutrientsContainer: {
flexDirection: 'row',
marginVertical: 4,
},
ingredientsContainer: {
flexDirection: 'row',
marginVertical: 4,
padding: 8,
backgroundColor: 'rgba(238, 242, 255, 1)',
},
ingredientTitle: {
marginHorizontal: 16,
fontWeight: '500',
fontSize: 14,
},
ingredientDetail: {
marginHorizontal: 16,
},
line: {
height: 0.5,
marginVertical: 8,
},
title: {
fontWeight: '500',
color: 'black',
fontSize: 16,
marginBottom: 12,
flex: 1,
},
servingContainerTitle: {
fontWeight: '400',
color: 'black',
fontSize: 13,
paddingVertical: 6,
overflow: 'hidden',
textTransform: 'capitalize',
},
nutrientTitle: {
fontWeight: '400',
color: 'black',
textTransform: 'capitalize',
flex: 1,
},
servingContainer: {
marginHorizontal: 4,
borderRadius: 12,
paddingHorizontal: 8,
backgroundColor: 'rgba(238, 242, 255, 1)',
},
servingSelectedContainer: {
backgroundColor: 'blue',
},
servingSelectedContainerTitle: {
color: 'white',
},
itemContainer: {
padding: 12,
backgroundColor: 'white',
marginVertical: 4,
marginHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
overflow: 'hidden',
fontSize: 16,
},
itemDetailContainer: {
padding: 16,
},
itemDetail: {
color: 'white',
},
itemIconContainer: {
overflow: 'hidden',
height: 60,
width: 60,
borderRadius: 30,
},
itemIcon: {
height: 60,
width: 60,
},
textInput: {
borderColor: 'rgba(209, 213, 219, 1)',
backgroundColor: 'rgba(238, 242, 255, 1)',
borderWidth: 1,
paddingHorizontal: 16,
borderRadius: 16,
padding: 16,
marginVertical: 16,
},
})
export default FoodDetail
export const getNutrientName: Record<string, string> = {
weight: 'Weight',
vitaminA: 'Vitamin A',
alcohol: 'Alcohol',
calcium: 'Calcium',
calories: 'Calories',
carbs: 'Carbohydrates',
cholesterol: 'Cholesterol',
fat: 'Fat',
fibers: 'Dietary Fiber',
iodine: 'Iodine',
iron: 'Iron',
magnesium: 'Magnesium',
monounsaturatedFat: 'Monounsaturated Fat',
phosphorus: 'Phosphorus',
polyunsaturatedFat: 'Polyunsaturated Fat',
potassium: 'Potassium',
protein: 'Protein',
satFat: 'Saturated Fat',
sodium: 'Sodium',
sugarAlcohol: 'Sugar Alcohol',
sugars: 'Sugars',
sugarsAdded: 'Added Sugars',
transFat: 'Trans Fat',
vitaminB12: 'Vitamin B12',
vitaminB12Added: 'Added Vitamin B12',
vitaminB6: 'Vitamin B6',
vitaminC: 'Vitamin C',
vitaminD: 'Vitamin D',
vitaminE: 'Vitamin E',
vitaminEAdded: 'Vitamin E Added',
}
FoodSearchView Refer to this to create an Item.
Last updated