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