Integrate Food Detail
/**
* Look up the nutrition attributes for a given Passio ID.
* @param passioID - The Passio ID for the attributes query.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
getAttributesForPassioID(passioID: PassioID): Promise<PassioFoodItem | null>
/**
* Query Passio's UPC web service for nutrition attributes of a given barcode.
* @param barcode - The barcode value for the attributes query, typically taken from a scanned `BarcodeCandidate`.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchAttributesForBarcode(barcode: Barcode): Promise<PassioFoodItem | null>
/**
* Query Passio's web service for nutrition attributes given an package food identifier.
* @param packagedFoodCode - The code identifier for the attributes query, taken from the list of package food candidates on a `FoodDetectionEvent`.
* @returns A `Promise` resolving to a `PassioFoodItem` object if the record exists in the database or `null` if not.
*/
fetchPassioIDAttributesForPackagedFood(
packagedFoodCode: PackagedFoodCode
): Promise<PassioFoodItem | null>
Example
import { useCallback, useState } from 'react'
import type {
PassioFoodItem,
PassioNutrients,
ServingUnit,
} 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
}
export const useFoodDetail = ({ passioFoodItem }: Props) => {
const defaultServing = {
unitName: passioFoodItem?.amount?.selectedUnit,
value: passioFoodItem?.amount?.weight?.value ?? 0,
}
const defaultCalculatedWeight = passioFoodItem?.amount?.weight?.value ?? 0
const [textInputServingQty, setTextInputServingQty] = useState<string>(
passioFoodItem?.amount?.selectedQuantity.toString() ?? ''
)
const [calculatedWeight, setCalculatedWeight] = useState<number>(
defaultCalculatedWeight
)
const [selectedServingUnit, setSelectedServingUnit] =
useState<ServingUnit>(defaultServing)
const onServingQuantityChange = useCallback(
(value: string) => {
const newQuantity = Number(value)
setTextInputServingQty(value)
if (newQuantity > 0) {
setCalculatedWeight((selectedServingUnit.value ?? 0) * newQuantity)
}
},
[selectedServingUnit.value]
)
const onServingSizeSelect = useCallback(
(value: ServingUnit) => {
const defaultWeight = calculatedWeight ?? 1
const newQuantity = Number((defaultWeight / value.value).toFixed(2))
setSelectedServingUnit(value)
setTextInputServingQty(newQuantity.toString())
},
[calculatedWeight]
)
function extractFoodNutrients(
passioNutrients: PassioNutrients
): FoodNutrient[] {
return Object.entries(passioNutrients).map(([title, { value, unit }]) => ({
title,
value: value,
unit,
}))
}
return {
calculatedWeight,
calculatedWeightUnit: passioFoodItem?.amount?.weight?.unit,
selectedServingUnit,
extractFoodNutrients,
onServingQuantityChange,
onServingSizeSelect,
passioFoodItem,
textInputServingQty,
}
}
import React from 'react'
import {
FlatList,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
Image,
View,
Pressable,
} from 'react-native'
import { useFoodDetail } from './useFoodDetail'
import {
PassioIconView,
IconSize,
PassioFoodItem,
PassioFoodItemNutrients,
} from '@passiolife/nutritionai-react-native-sdk-v3'
interface Props {
onClose: () => void
passioFoodItem: PassioFoodItem
}
const FoodDetail = ({ onClose, passioFoodItem }: Props) => {
const styles = foodDetailStyle()
const {
calculatedWeight,
calculatedWeightUnit,
selectedServingUnit,
extractFoodNutrients,
onServingQuantityChange,
onServingSizeSelect,
textInputServingQty,
} = useFoodDetail({ passioFoodItem })
console.log(passioFoodItem)
const nutrients = extractFoodNutrients(
new PassioFoodItemNutrients(passioFoodItem).nutrients({
unit: calculatedWeightUnit ?? 'g',
value: calculatedWeight,
})
)
const renderNutrientItem = () => (
<View style={styles.nutrientContainer}>
<Text style={styles.title}>Nutrients</Text>
<FlatList
data={nutrients}
renderItem={({ item }) => {
return (
<View style={styles.nutrientsContainer}>
<Text style={styles.nutrientTitle}>{item.title}</Text>
<Text>
{item.value.toFixed(2)} {item.unit}
</Text>
</View>
)
}}
/>
</View>
)
const renderEditServing = () => {
return (
<View style={styles.nutrientContainer}>
<Text style={styles.title}>{`Serving Sizes (${calculatedWeight.toFixed(
2
)} ${calculatedWeightUnit})`}</Text>
<TextInput
value={textInputServingQty}
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.unitName === item.unitName &&
styles.servingSelectedContainer,
]}
>
<Text
style={[
styles.servingContainerTitle,
selectedServingUnit.unitName === item.unitName &&
styles.servingSelectedContainerTitle,
]}
>
{item.unitName} ({item.value})
</Text>
</Pressable>
)}
/>
</View>
)
}
const renderIngredient = () => {
return (
<View style={styles.nutrientContainer}>
<Text style={styles.title}>Ingredients</Text>
<FlatList
data={passioFoodItem.ingredients}
renderItem={({ item }) => (
<View style={styles.ingredientsContainer}>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: item.iconId,
iconSize: IconSize.PX360,
}}
/>
</View>
<View style={{ alignSelf: 'center' }}>
<Text style={styles.ingredientTitle}>{item.name}</Text>
<Text style={styles.ingredientDetail}>
{item.weight?.value} {item.weight?.unit},{' '}
{item.amount?.selectedQuantity} {item.amount?.selectedUnit}
</Text>
</View>
</View>
)}
/>
</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={{ flex: 1, marginTop: 12 }}>
<Text style={{ fontSize: 16, fontWeight: '600' }}>{title}</Text>
<Text>{(nutrient?.value ?? 0).toFixed(2)}</Text>
</View>
)
})}
</View>
)
}
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.PX360,
}}
/>
</View>
<View style={styles.flex1}>
<Text style={styles.foodName}>{passioFoodItem.name}</Text>
<Text style={styles.foodDetail}>
{calculatedWeight.toFixed(2)} {calculatedWeightUnit} |{' '}
{selectedServingUnit.value.toFixed(2)}{' '}
{selectedServingUnit.unitName}
</Text>
{passioFoodItem.isOpenFood && (
<Text style={styles.foodDetail}>This item is Open food</Text>
)}
</View>
</View>
{renderMacro()}
</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>
</SafeAreaView>
)
}
const foodDetailStyle = () =>
StyleSheet.create({
container: {
backgroundColor: 'rgba(242, 245, 251, 1)',
flex: 1,
},
flex1: {
flex: 1,
},
macroContainer: {
flexDirection: 'row',
flex: 1,
},
closeButton: {
margin: 16,
},
itemFoodInfo: {
flexDirection: 'row',
alignSelf: 'center',
justifyContent: 'center',
flex: 1,
},
itemFoodInfoContainer: {
borderRadius: 16,
backgroundColor: 'white',
padding: 16,
},
foodName: {
paddingHorizontal: 16,
fontSize: 16,
textTransform: 'capitalize',
fontWeight: '600',
},
foodDetail: {
paddingHorizontal: 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,
backgroundColor: 'white',
},
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
Last updated