Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
/**
* Configure the SDK with the given options.
*
* @param options - The configuration options
* @returns A `Promise` resolving with a `PassioStatus` indicating the current state of the SDK.
*/
configure(options: ConfigurationOptions): Promise<PassioStatus>
/**
* Prompt the user for camera authorization if not already granted.
* @remarks Your app's Info.plist must inclue an `NSCameraUsageDescription` value or this method will crash.
* @returns A `Promise` resolving to `true` if authorization has been granted or `false` if not.
*/
requestCameraAuthorization(): Promise<boolean>
/**
* This method indicating downloading file status if model download from passio server.
* @param callback - A callback to receive for downloading file lefts from queue.
* @param callback - A callback to receive download file failed for some reason.
* @returns A `Callback` that should be retained by the caller while downloading is running. Call `remove` on the callback to terminate listeners and relase from memory.
*/
onDownloadingPassioModelCallBacks: (
downloadModelCallBack: DownloadModelCallBack
) => Callback
import { useEffect, useState } from 'react'
import {
CompletedDownloadingFile,
DownloadingError,
PassioSDK,
} from '@passiolife/nutritionai-react-native-sdk-v3'
export type SDKStatus = 'init' | 'downloading' | 'error' | 'ready'
export const usePassioSDK = ({
key,
debugMode = false,
autoUpdate = false,
}: {
key: string
debugMode?: boolean
autoUpdate?: boolean
}) => {
const [loadingState, setLoadingState] = useState<SDKStatus>('init')
const [leftFile, setDownloadingLeft] = useState<number | null>(null)
useEffect(() => {
async function configure() {
try {
const status = await PassioSDK.configure({
key: key,
debugMode: debugMode,
autoUpdate: autoUpdate,
})
switch (status.mode) {
case 'notReady':
return
case 'isReadyForDetection':
setLoadingState('ready')
return
case 'error':
console.error(`PassioSDK Error ${status.errorMessage}`)
setLoadingState('error')
return
}
} catch (err) {
console.error(`PassioSDK Error ${err}`)
setLoadingState('error')
}
}
configure()
}, [key, debugMode, autoUpdate])
useEffect(() => {
const callBacks = PassioSDK.onDownloadingPassioModelCallBacks({
completedDownloadingFile: ({ filesLeft }: CompletedDownloadingFile) => {
setDownloadingLeft(filesLeft)
},
downloadingError: ({ message }: DownloadingError) => {
console.log('DownloadingError ===>', message)
},
})
return () => callBacks.remove()
}, [])
return {
loadingState,
leftFile,
}
}
export const useCameraAuthorization = () => {
const [authorized, setAuthorized] = useState(false)
useEffect(() => {
async function getAuth() {
const isAuthorized = await PassioSDK.requestCameraAuthorization()
setAuthorized(isAuthorized)
}
getAuth()
}, [])
return authorized
}
The API below is used for food search.
/**
* This method detect food from image uri.
* @param imageUri - The image uri to detect food.
* @returns A `Promise` resolving to a `PassioAdvisorFoodInfo` array or `null`.
*/
recognizeImageRemote(
imageUri: string
): Promise<PassioAdvisorFoodInfo[] | null>
import { useState, useCallback } from 'react'
import {
PassioAdvisorFoodInfo,
PassioSDK,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import type { Props } from './RecognizeRemote'
import { launchImageLibrary } from 'react-native-image-picker'
import { Alert } from 'react-native'
const useRecognizeRemote = ({}: Props) => {
const [loading, setLoading] = useState<boolean>(false)
const [passioSpeechRecognitionModel, setPassioSpeechRecognitionModel] =
useState<PassioAdvisorFoodInfo[] | null>()
const onScanImage = useCallback(async () => {
try {
const { assets } = await launchImageLibrary({ mediaType: 'photo' })
if (assets) {
setLoading(true)
setPassioSpeechRecognitionModel(null)
PassioSDK.recognizeImageRemote(
assets?.[0].uri?.replace('file://', '') ?? ''
)
.then(async (candidates) => {
setPassioSpeechRecognitionModel(candidates)
})
.catch(() => {
Alert.alert('Unable to recognized this image')
})
.finally(() => {
setLoading(false)
})
}
} catch (err) {
setLoading(false)
}
}, [])
return {
onScanImage,
passioSpeechRecognitionModel,
loading,
}
}
export default useRecognizeRemote
import React from 'react'
import {
ActivityIndicator,
FlatList,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
Image,
View,
} from 'react-native'
import {
PassioIconView,
IconSize,
PassioSDK,
PassioFoodItem,
PassioAdvisorFoodInfo,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import useRecognizeRemote from './useRecognizeRemote'
export interface Props {
onClose: () => void
onFoodDetail: (passioFoodItem: PassioFoodItem) => void
}
// FoodSearchScreen component
export const RecognizeImageRemote = (props: Props) => {
// Get styles object from the searchStyle function
const styles = searchStyle()
// Destructure values from the custom hook
const { loading, passioSpeechRecognitionModel, onScanImage } =
useRecognizeRemote(props)
// Function to render each item in the FlatList
const renderSearchItem = ({ item }: { item: PassioAdvisorFoodInfo }) => {
return (
<TouchableOpacity
style={styles.itemContainer}
onPress={async () => {
if (item.foodDataInfo) {
const dataInfo = await PassioSDK.fetchFoodItemForDataInfo(
item?.foodDataInfo
)
if (dataInfo) {
props.onFoodDetail(dataInfo)
}
}
}}
>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: item?.foodDataInfo?.iconID ?? '',
iconSize: IconSize.PX360,
}}
/>
</View>
<View>
<Text style={styles.itemFoodName}>{item?.recognizedName}</Text>
<Text style={styles.itemFoodName}>
{item?.portionSize + ' | ' + item.weightGrams}
</Text>
</View>
</TouchableOpacity>
)
}
// Display loading indicator when results are empty and loading is true
const renderLoading = () => {
return <>{loading ? <ActivityIndicator /> : null}</>
}
// Render the component
return (
<SafeAreaView style={styles.body}>
<View style={styles.closeButton}>
<TouchableOpacity onPress={props.onClose}>
<Image
style={styles.closeText}
source={require('../assets/back.png')}
/>
</TouchableOpacity>
</View>
{/* Search input */}
<TouchableOpacity onPress={onScanImage} style={styles.textInput}>
<Text style={{ color: 'white' }}>Pick Image</Text>
</TouchableOpacity>
<FlatList
data={passioSpeechRecognitionModel}
contentContainerStyle={styles.list}
renderItem={renderSearchItem}
ListEmptyComponent={renderLoading}
keyExtractor={(index) => 'item.advisorInfo?.foodDataInfo' + index}
/>
</SafeAreaView>
)
}
// Styles for the component
const searchStyle = () =>
StyleSheet.create({
closeButton: {},
list: {
marginTop: 16,
},
closeText: {
margin: 16,
height: 24,
width: 24,
},
itemContainer: {
padding: 12,
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
backgroundColor: 'white',
flexDirection: 'row',
borderRadius: 24,
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 16,
},
itemBrandName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 12,
},
itemAlternativeContainer: {
overflow: 'hidden',
},
alternativeContainer: {
marginStart: 16,
alignItems: 'center',
overflow: 'hidden',
alignSelf: 'center',
backgroundColor: 'rgba(238, 242, 255, 1)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 0.1 },
shadowOpacity: 0.5,
shadowRadius: 0.5,
marginVertical: 2,
marginBottom: 14,
elevation: 5,
borderRadius: 24,
},
itemAlternativeName: {
textTransform: 'capitalize',
paddingVertical: 8,
paddingHorizontal: 16,
},
itemIconContainer: {
height: 46,
width: 46,
borderRadius: 30,
overflow: 'hidden',
},
itemIcon: {
height: 46,
width: 46,
},
textInput: {
backgroundColor: 'blue',
paddingHorizontal: 16,
padding: 12,
alignItems: 'center',
color: 'white',
borderRadius: 16,
fontWeight: '500',
fontSize: 16,
marginHorizontal: 16,
},
body: {
backgroundColor: 'rgba(242, 245, 251, 1)',
flex: 1,
},
})
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>
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,
}
}
FoodSearchView Refer to this to create an Item.
The API below is used for Quick Scan.
Make sure the integration setup done before this implementation
/**
* Begin food detection using the device's camera.
* @param options - An object to determine which types of scanning should be performed.
* @param callback - A callback to repeatedly receive food detection events as they occur.
* @returns A `Subscription` that should be retained by the caller while food detection is running. Call `remove` on the subscription to terminate food detection.
*/
startFoodDetection(
options: FoodDetectionConfig,
callback: (detection: FoodDetectionEvent) => void
): Subscription
/**
* 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 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>
import { useEffect, useRef, useState, useCallback } from 'react'
import {
PassioSDK,
type FoodDetectionConfig,
type FoodDetectionEvent,
PassioFoodItem,
DetectedCandidate,
} from '@passiolife/nutritionai-react-native-sdk-v3'
/**
* Custom hook for handling quick food scanning using PassioSDK.
* It provides functions and state variables related to food detection and alternative food items.
*/
export const useQuickScan = () => {
// State variables
const [passioFoodItem, setPassioFoodItem] = useState<PassioFoodItem | null>(
null
)
const [alternative, setAlternativePassioIDAttributes] = useState<
DetectedCandidate[] | null | undefined
>(null)
const [loading, setLoading] = useState(true)
const passioFoodItemRef = useRef<PassioFoodItem | null>(null)
// Function to clear the scanning results
const onClearResultPress = () => {
setLoading(true)
passioFoodItemRef.current = null
setPassioFoodItem(null)
setAlternativePassioIDAttributes(null)
}
useEffect(() => {
// Function to handle food detection events
const handleFoodDetection = async (detection: FoodDetectionEvent) => {
const { candidates } = detection
// If no candidates available, return
if (!candidates) {
return
}
let attributes: PassioFoodItem | null = null
// Determine the type of food detection and fetch attributes accordingly
if (candidates.barcodeCandidates?.[0]) {
const barcode = candidates.barcodeCandidates[0].barcode
attributes = await PassioSDK.fetchFoodItemForProductCode(barcode)
} else if (candidates.packagedFoodCode?.[0]) {
const packagedFoodCode = candidates.packagedFoodCode[0]
attributes = await PassioSDK.fetchFoodItemForProductCode(
packagedFoodCode
)
} else if (candidates.detectedCandidates?.[0]) {
const passioID = candidates.detectedCandidates[0].passioID
attributes = await PassioSDK.fetchFoodItemForPassioID(passioID)
}
// If attributes are null, return
if (attributes === null) {
return
}
// Check if the detected food is different from the previous one
if (attributes?.id !== passioFoodItemRef.current?.id) {
passioFoodItemRef.current = attributes
// Update state variables and fetch alternative food items
setPassioFoodItem((prev) => {
if (attributes?.id === prev?.id) {
return prev
} else {
setAlternativePassioIDAttributes(
candidates.detectedCandidates[0]?.alternatives
)
return attributes
}
})
setLoading(false)
}
}
// Configuration for food detection
const config: FoodDetectionConfig = {
detectBarcodes: true,
detectPackagedFood: true,
}
// Start food detection and subscribe to events
const subscription = PassioSDK.startFoodDetection(
config,
handleFoodDetection
)
// Cleanup function to unsubscribe when the component unmounts
return () => subscription.remove()
}, []) // Empty dependency array to run the effect only once during component mount
// Function to handle changes in alternative food items
const onAlternativeFoodItemChange = useCallback(
async (attribute: DetectedCandidate) => {
const alternatePassioFoodItem = await PassioSDK.fetchFoodItemForPassioID(
attribute.passioID
)
if (alternatePassioFoodItem) {
passioFoodItemRef.current = alternatePassioFoodItem
setPassioFoodItem(alternatePassioFoodItem)
}
},
[]
)
// Return the hook's public API
return {
loading,
passioFoodItem,
onAlternativeFoodItemChange,
onClearResultPress,
alternative,
}
}
import {
ActivityIndicator,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {
DetectionCameraView,
PassioFoodItem,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import { useQuickScan } from './useQuickScan'
import React from 'react'
import { QuickFoodResult } from './view/QuickFoodResult'
interface Props {
onClose: () => void
onFoodDetail: (passioFoodItem: PassioFoodItem) => void
}
export const QuickScanningScreen = ({ onClose, onFoodDetail }: Props) => {
const {
loading,
passioFoodItem,
onClearResultPress,
alternative,
onAlternativeFoodItemChange,
} = useQuickScan()
const styles = quickScanStyle()
return (
<View style={styles.blackBackgroundStyle}>
<DetectionCameraView style={styles.detectionCamera} />
{loading ? (
<View style={styles.loadingIndicator}>
<ActivityIndicator />
<Text>Scanning...</Text>
</View>
) : null}
{passioFoodItem !== null ? (
<QuickFoodResult
attribute={passioFoodItem}
onAlternativeFoodItemChange={onAlternativeFoodItemChange}
onClearResultPress={onClearResultPress}
alternativeAttributes={alternative ?? []}
onItemClick={onFoodDetail}
/>
) : null}
<View style={styles.closeButton}>
<TouchableOpacity onPress={onClose}>
<Text style={styles.text}>✕</Text>
</TouchableOpacity>
</View>
</View>
)
}
const quickScanStyle = () =>
StyleSheet.create({
detectionCamera: {
flex: 1,
width: '100%',
},
blackBackgroundStyle: {
backgroundColor: 'black',
width: '100%',
flex: 1,
flexDirection: 'column',
},
loadingIndicator: {
backgroundColor: 'white',
minHeight: 150,
borderTopRightRadius: 24,
alignItems: 'center',
justifyContent: 'center',
borderTopLeftRadius: 24,
position: 'absolute',
bottom: 0,
right: 0,
left: 0,
},
text: {
color: 'white',
fontSize: 30,
},
closeButton: {
position: 'absolute',
top: 45,
right: 25,
zIndex: 1000,
color: 'white',
},
})
import React from 'react'
import { Pressable, StyleSheet, Text, View, Image } from 'react-native'
import {
IconSize,
PassioIconView,
type PassioFoodItem,
DetectedCandidate,
PassioSDK,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import { AlternativeFood } from '../../../src/views/AlternativeFood'
export interface QuickFoodResultProps {
attribute: PassioFoodItem
alternativeAttributes?: DetectedCandidate[]
onAlternativeFoodItemChange?: (item: DetectedCandidate) => void
onClearResultPress?: () => void
onItemClick?: (passioFoodItem: PassioFoodItem) => void
}
export const QuickFoodResult = ({
attribute,
alternativeAttributes,
onAlternativeFoodItemChange,
onClearResultPress,
onItemClick,
}: QuickFoodResultProps) => {
const styles = quickFoodResultStyle()
const nutrients =
PassioSDK.getNutrientsSelectedSizeOfPassioFoodItem(attribute)
return (
<Pressable
onPress={() => {
onItemClick?.(attribute)
}}
style={styles.itemContainer}
>
<View style={styles.foodResult}>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: attribute.iconId ?? attribute.id,
iconSize: IconSize.PX180,
}}
/>
</View>
<View>
<Text style={styles.itemFoodName}>{attribute.name}</Text>
<Text style={styles.itemFoodDetail}>
{Math.round(attribute.amount?.weight?.value ?? 0) +
' ' +
attribute.amount?.weight?.unit}
</Text>
<Text style={styles.itemFoodDetail}>
{Math.round(nutrients.calories?.value ?? 0) +
' ' +
nutrients?.calories?.unit}
</Text>
</View>
</View>
{alternativeAttributes && (
<AlternativeFood
detectedCandidates={alternativeAttributes}
onAlternativeFoodItemChange={onAlternativeFoodItemChange}
/>
)}
<Pressable style={styles.clearResult} onPress={onClearResultPress}>
<Image
source={require('../../assets/close.png')}
style={styles.close}
/>
</Pressable>
</Pressable>
)
}
const quickFoodResultStyle = () =>
StyleSheet.create({
itemContainer: {
position: 'absolute',
bottom: 0,
right: 0,
padding: 16,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
backgroundColor: 'white',
minHeight: 150,
flex: 1,
marginVertical: 0,
left: 0,
},
foodResult: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
close: {
width: 24,
height: 24,
tintColor: 'white',
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
paddingHorizontal: 8,
fontSize: 16,
fontWeight: '500',
},
itemFoodDetail: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 12,
},
itemIcon: {
height: 60,
width: 60,
},
itemIconContainer: {
height: 60,
width: 60,
overflow: 'hidden',
borderRadius: 30,
},
clearResult: {
flexDirection: 'row',
backgroundColor: 'red',
borderRadius: 32,
alignItems: 'center',
alignSelf: 'center',
padding: 8,
marginVertical: 16,
},
})
Passio provide searchForFood API to get FoodSearchResult
The API below is used for food search.
/**
* Search the database of foods with a given search term.
* @param searchQuery - The search term to match against food item names.
* @returns A `Promise` resolving to an array of food item names.
*/
searchForFood(searchQuery: string): Promise<PassioSearchResult | null>
/**
* Data info of the search food with a given search result.
* @param passioFoodDataInfo - Provide `PassioFoodDataInfo` object get `PassioFoodItem` detail.
* @returns A `Promise` resolving to `PassioFoodItem` detail.
*/
fetchFoodItemForDataInfo(
passioFoodDataInfo: PassioFoodDataInfo
): Promise<PassioFoodItem | null>
import { useState, useCallback, useEffect } from 'react'
import {
PassioSDK,
type PassioFoodDataInfo,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import { useDebounce } from '../utils/common'
import type { Props } from './FoodSearch'
const useFoodSearch = ({ onFoodDetail }: Props) => {
// State variables
const [searchQuery, setSearchQuery] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const [foodResults, setFoodResults] = useState<PassioFoodDataInfo[] | null>()
const [alternatives, setAlternative] = useState<string[] | null>()
const debouncedSearchTerm: string = useDebounce<string>(searchQuery, 500)
// Clears search results and resets state
const cleanSearch = useCallback(() => {
setSearchQuery('')
setFoodResults([])
setLoading(false)
}, [])
// Calls the search API based on the input value
const callSearchApi = useCallback(
async (query: string) => {
// Check if the query is not empty
if (query.length > 0) {
// Set loading state to indicate the start of the search
setLoading(true)
try {
// Fetch food results from the PassioSDK based on the query
const searchFoods = await PassioSDK.searchForFood(query)
setFoodResults(searchFoods?.results)
setAlternative(searchFoods?.alternatives)
} catch (error) {
// Handle errors, e.g., network issues or API failures
setFoodResults([])
} finally {
// Reset loading state to indicate the end of the search
setLoading(false)
}
} else {
// If the query is empty, reset the search state
cleanSearch()
}
},
[cleanSearch]
)
// Initiates a new search with the provided query
const onSearchFood = useCallback(
async (q: string) => {
if (q.length > 0) {
setSearchQuery(q)
setAlternative([])
setFoodResults([])
} else {
cleanSearch()
}
},
[cleanSearch]
)
// Effect for handling debounced search term changes
useEffect(() => {
if (debouncedSearchTerm.length > 0) {
callSearchApi(debouncedSearchTerm)
} else {
cleanSearch()
}
}, [callSearchApi, debouncedSearchTerm, cleanSearch])
const onSearchResultItemPress = useCallback(
async (foodSearchResult: PassioFoodDataInfo) => {
// Achieved Result through `fetchSearchResult`
const result = await PassioSDK.fetchFoodItemForDataInfo(foodSearchResult)
if (result) {
onFoodDetail(result)
}
},
[onFoodDetail]
)
return {
alternatives,
cleanSearch,
foodResults,
loading,
onSearchFood,
onSearchResultItemPress,
searchQuery,
}
}
export default useFoodSearch
import React from 'react'
import {
ActivityIndicator,
FlatList,
Pressable,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
Image,
View,
} from 'react-native'
import {
PassioIconView,
IconSize,
PassioFoodDataInfo,
PassioFoodItem,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import useFoodSearch from './useSearch'
export interface Props {
onClose: () => void
onFoodDetail: (passioFoodItem: PassioFoodItem) => void
}
// FoodSearchScreen component
export const FoodSearchView = (props: Props) => {
// Get styles object from the searchStyle function
const styles = searchStyle()
// Destructure values from the custom hook
const {
searchQuery,
onSearchFood,
foodResults,
loading,
alternatives,
onSearchResultItemPress,
} = useFoodSearch(props)
// Function to render each item in the FlatList
const renderSearchItem = ({ item }: { item: PassioFoodDataInfo }) => {
return (
<TouchableOpacity
style={styles.itemContainer}
onPress={() => onSearchResultItemPress(item)}
>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: item.iconID,
iconSize: IconSize.PX360,
}}
/>
</View>
<View>
<Text style={styles.itemFoodName}>{item.foodName}</Text>
<Text style={styles.itemBrandName}>
{'calories ' +
Math.round(item.nutritionPreview?.calories ?? 0) +
' kcal | '}
<Text style={styles.itemBrandName}>
{'fat ' + Math.round(item.nutritionPreview?.fat ?? 0)}
</Text>
<Text style={styles.itemBrandName}>
{' | protein ' + Math.round(item.nutritionPreview?.protein ?? 0)}
</Text>
</Text>
<Text style={styles.itemBrandName}>
{'carbs ' + Math.round(item.nutritionPreview?.carbs ?? 0)}
</Text>
</View>
</TouchableOpacity>
)
}
const renderAlternativeItem = ({ item }: { item: string }) => {
return (
<Pressable
style={styles.alternativeContainer}
onPress={() => onSearchFood(item)}
>
<Text style={styles.itemAlternativeName}>{item}</Text>
</Pressable>
)
}
// Display loading indicator when results are empty and loading is true
const renderLoading = () => {
return <>{loading ? <ActivityIndicator /> : null}</>
}
// Render the component
return (
<SafeAreaView style={styles.body}>
<View style={styles.closeButton}>
<TouchableOpacity onPress={props.onClose}>
<Image
style={styles.closeText}
source={require('../assets/back.png')}
/>
</TouchableOpacity>
</View>
{/* Search input */}
<TextInput
value={searchQuery}
style={styles.textInput}
placeholder={'Type in food name'}
placeholderTextColor={'gray'}
onChangeText={onSearchFood}
/>
<FlatList
data={foodResults}
contentContainerStyle={styles.list}
renderItem={renderSearchItem}
ListEmptyComponent={renderLoading}
ListHeaderComponent={() => (
<FlatList
data={alternatives}
contentContainerStyle={styles.itemAlternativeContainer}
horizontal
showsHorizontalScrollIndicator={false}
renderItem={renderAlternativeItem}
keyExtractor={(item, index) => item.toString() + index}
/>
)}
keyExtractor={(item, index) => item.iconID.toString() + index}
/>
</SafeAreaView>
)
}
// Styles for the component
const searchStyle = () =>
StyleSheet.create({
closeButton: {},
list: {
marginTop: 16,
},
closeText: {
margin: 16,
height: 24,
width: 24,
},
itemContainer: {
padding: 12,
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
backgroundColor: 'white',
flexDirection: 'row',
borderRadius: 24,
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 16,
},
itemBrandName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 12,
},
itemAlternativeContainer: {
overflow: 'hidden',
},
alternativeContainer: {
marginStart: 16,
alignItems: 'center',
overflow: 'hidden',
alignSelf: 'center',
backgroundColor: 'rgba(238, 242, 255, 1)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 0.1 },
shadowOpacity: 0.5,
shadowRadius: 0.5,
marginVertical: 2,
marginBottom: 14,
elevation: 5,
borderRadius: 24,
},
itemAlternativeName: {
textTransform: 'capitalize',
paddingVertical: 8,
paddingHorizontal: 16,
},
itemIconContainer: {
height: 46,
width: 46,
borderRadius: 30,
overflow: 'hidden',
},
itemIcon: {
height: 46,
width: 46,
},
textInput: {
backgroundColor: 'white',
paddingHorizontal: 16,
padding: 12,
color: 'black',
fontWeight: '500',
fontSize: 16,
marginHorizontal: 16,
},
body: {
backgroundColor: 'rgba(242, 245, 251, 1)',
flex: 1,
},
})
import { useState, useEffect } from 'react'
// Hook
// T is a generic type for value parameter, our case this will be string
export function useDebounce<T>(value: T, delay: number): T {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler)
}
},
[value, delay] // Only re-call effect if value or delay changes
)
return debouncedValue
}
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',
}
The API below is used for food search.
/**
* fetch list of all meal Plans
* @returns A `Promise` resolving to a `PassioMealPlan` array if the record exists in the database or `null` if not.
*/
fetchMealPlans(): Promise<PassioMealPlan[] | null>
/**
* fetch list of all meal Plan item
* @param mealPlanLabel - query for type of mealPlan.
* @param day - for which day meal plan is needed
* @returns A `Promise` resolving to a `PassioMealPlanItem` array if the record exists in the database or `null` if not.
*/
fetchMealPlanForDay(
mealPlanLabel: string,
day: number
): Promise<PassioMealPlanItem[] | null>
import { useState, useCallback, useEffect, useMemo } from 'react'
import {
PassioSDK,
type PassioFoodDataInfo,
PassioMealPlan,
PassioMealPlanItem,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import type { Props } from './MealPlan'
const useMealPlan = ({ onFoodDetail }: Props) => {
// State variables
const [passioMealPlan, setPassioMealPlan] = useState<PassioMealPlan>()
const [selectedDay, setSelectedDay] = useState<number>(0)
const [passioMealPlans, setPassioMealPlans] = useState<PassioMealPlan[]>()
const [loading, setLoading] = useState<boolean>(false)
const [passioMealPlanItem, setPassioMealPlanItem] = useState<
PassioMealPlanItem[] | null
>()
const generateDaysData = useMemo(() => {
const days = []
for (let i = 1; i <= 14; i++) {
days.push(`Day ${i}`)
}
return days
}, [])
const onChangeDay = useCallback((day: number) => {
setSelectedDay(day)
}, [])
// Effect for handling debounced search term changes
useEffect(() => {
async function init() {
try {
setLoading(true)
setPassioMealPlanItem([])
setPassioMealPlans([])
// Fetch food results from the PassioSDK based on the query
const mealPlans = await PassioSDK.fetchMealPlans()
if (mealPlans) {
setPassioMealPlans(mealPlans)
const initial = mealPlans[0]
if (initial) {
setPassioMealPlan(initial)
}
}
} catch (error) {
setPassioMealPlans([])
setPassioMealPlanItem([])
} finally {
// Reset loading state to indicate the end of the search
setLoading(false)
}
}
init()
}, [])
// Effect for handling debounced search term changes
useEffect(() => {
async function init() {
try {
setLoading(true)
setPassioMealPlanItem([])
if (passioMealPlan?.mealPlanLabel) {
const mealPlans = await PassioSDK.fetchMealPlanForDay(
passioMealPlan?.mealPlanLabel,
selectedDay + 1
)
setPassioMealPlanItem(mealPlans)
}
} catch (error) {
// Handle errors, e.g., network issues or API failures
setPassioMealPlanItem([])
} finally {
// Reset loading state to indicate the end of the search
setLoading(false)
}
}
init()
}, [passioMealPlan, passioMealPlan?.mealPlanLabel, selectedDay])
const onResultItemPress = useCallback(
async (foodSearchResult: PassioFoodDataInfo) => {
let result = await PassioSDK.fetchFoodItemForDataInfo(foodSearchResult)
if (result) {
if (foodSearchResult.nutritionPreview?.weightUnit) {
result.amount.weight = {
unit: foodSearchResult.nutritionPreview?.weightUnit,
value: foodSearchResult.nutritionPreview?.weightQuantity ?? 0,
}
result.amount.selectedUnit =
foodSearchResult.nutritionPreview?.servingUnit
result.amount.selectedQuantity =
foodSearchResult.nutritionPreview?.servingQuantity ?? 0
}
console.log(JSON.stringify(result))
onFoodDetail(result)
}
},
[onFoodDetail]
)
const onChangeMeal = (plan: PassioMealPlan) => {
setPassioMealPlan(plan)
}
return {
passioMealPlanItem,
passioMealPlans,
onChangeMeal,
loading,
onResultItemPress,
passioMealPlan,
generateDaysData,
onChangeDay,
selectedDay,
}
}
export default useMealPlan
import React from 'react'
import {
ActivityIndicator,
FlatList,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
Image,
View,
} from 'react-native'
import {
PassioIconView,
IconSize,
PassioFoodItem,
PassioMealPlan,
PassioMealPlanItem,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import useMealPlan from './useMealPlan'
export interface Props {
onClose: () => void
onFoodDetail: (passioFoodItem: PassioFoodItem) => void
}
// FoodSearchScreen component
export const MealPlan = (props: Props) => {
// Get styles object from the searchStyle function
const styles = searchStyle()
// Destructure values from the custom hook
const {
passioMealPlanItem,
passioMealPlans,
onChangeMeal,
loading,
onResultItemPress,
passioMealPlan,
generateDaysData,
onChangeDay,
selectedDay,
} = useMealPlan(props)
// Function to render each item in the FlatList
const renderSearchItem = ({ item }: { item: PassioMealPlanItem }) => {
return (
<TouchableOpacity
style={styles.itemContainer}
onPress={() => onResultItemPress(item.meal)}
>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: item.meal.iconID,
iconSize: IconSize.PX360,
}}
/>
</View>
<View>
<Text style={styles.itemFoodName}>{item.meal.foodName}</Text>
<Text style={styles.itemBrandName}>
{item.meal.nutritionPreview?.servingUnit + ' '}
{item.meal.nutritionPreview?.servingQuantity + ' | '}
{Math.round(item.meal.nutritionPreview?.calories ?? 0) + ' kcal'}
{item.meal.brandName ? ', ' + item.meal.brandName : ''}
</Text>
</View>
</TouchableOpacity>
)
}
const renderMealTime = ({ item }: { item: PassioMealPlan }) => {
return (
<TouchableOpacity
style={styles.mealTimeContainer}
onPress={() => onChangeMeal(item)}
>
<Text
style={[
styles.mealTime,
item.mealPlanLabel === passioMealPlan?.mealPlanLabel &&
styles.selectedMealTime,
]}
>
{item.mealPlanLabel}
</Text>
</TouchableOpacity>
)
}
const renderDays = ({ item, index }: { item: string; index: number }) => {
return (
<TouchableOpacity
style={styles.mealTimeContainer}
onPress={() => {
onChangeDay(index)
}}
>
<Text
style={[
styles.mealTime,
index === selectedDay && styles.selectedMealTime,
]}
>
{item}
</Text>
</TouchableOpacity>
)
}
// Display loading indicator when results are empty and loading is true
const renderLoading = () => {
return (
<>{loading ? <ActivityIndicator style={{ marginTop: 100 }} /> : null}</>
)
}
// Render the component
return (
<SafeAreaView style={styles.body}>
<View style={styles.closeButton}>
<TouchableOpacity onPress={props.onClose}>
<Image
style={styles.closeText}
source={require('../assets/back.png')}
/>
</TouchableOpacity>
</View>
<FlatList
data={passioMealPlanItem}
contentContainerStyle={styles.list}
renderItem={renderSearchItem}
ListEmptyComponent={renderLoading}
ListHeaderComponent={() => {
return (
<View>
<FlatList
data={generateDaysData}
renderItem={renderDays}
horizontal
/>
<FlatList
data={passioMealPlans}
renderItem={renderMealTime}
horizontal
/>
</View>
)
}}
keyExtractor={(item, index) => item.meal.foodName.toString() + index}
/>
</SafeAreaView>
)
}
// Styles for the component
const searchStyle = () =>
StyleSheet.create({
closeButton: {},
list: {
marginTop: 16,
},
closeText: {
margin: 16,
height: 24,
width: 24,
},
itemContainer: {
padding: 12,
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
backgroundColor: 'white',
flexDirection: 'row',
borderRadius: 24,
},
mealTimeContainer: {
marginStart: 16,
backgroundColor: 'white',
flexDirection: 'row',
borderRadius: 24,
marginBottom: 16,
overflow: 'hidden',
},
mealTime: {
textTransform: 'capitalize',
paddingVertical: 12,
paddingHorizontal: 12,
fontSize: 16,
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 16,
},
selectedMealTime: {
color: 'white',
backgroundColor: 'blue',
},
itemBrandName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 12,
},
itemAlternativeContainer: {
overflow: 'hidden',
},
alternativeContainer: {
marginStart: 16,
alignItems: 'center',
overflow: 'hidden',
alignSelf: 'center',
backgroundColor: 'rgba(238, 242, 255, 1)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 0.1 },
shadowOpacity: 0.5,
shadowRadius: 0.5,
marginVertical: 2,
marginBottom: 14,
elevation: 5,
borderRadius: 24,
},
itemAlternativeName: {
textTransform: 'capitalize',
paddingVertical: 8,
paddingHorizontal: 16,
},
itemIconContainer: {
height: 46,
width: 46,
borderRadius: 30,
overflow: 'hidden',
},
itemIcon: {
height: 46,
width: 46,
},
textInput: {
backgroundColor: 'white',
paddingHorizontal: 16,
padding: 12,
color: 'black',
fontWeight: '500',
fontSize: 16,
marginHorizontal: 16,
},
body: {
backgroundColor: 'rgba(242, 245, 251, 1)',
flex: 1,
},
})
The API below is used for food search.
/**
* fetch a suggestions for particular meal time 'breakfast' | 'lunch' | 'dinner' | 'snack' and returning results.
* @param mealTime - 'breakfast' | 'lunch' | 'dinner' | 'snack',
* @returns A `Promise` resolving to a `PassioFoodDataInfo` array if the record exists in the database or `null` if not.
*/
fetchSuggestions(
mealTime: PassioMealTime
): Promise<PassioFoodDataInfo[] | null>
/**
* Data info of the search food with a given search result.
* @param passioFoodDataInfo - Provide `PassioFoodDataInfo` object get `PassioFoodItem` detail.
* @returns A `Promise` resolving to `PassioFoodItem` detail.
*/
fetchFoodItemForDataInfo(
passioFoodDataInfo: PassioFoodDataInfo
): Promise<PassioFoodItem | null>
import { useState, useCallback, useEffect } from 'react'
import {
PassioSDK,
type PassioFoodDataInfo,
PassioMealTime,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import type { Props } from './FoodSuggestion'
const useSuggestions = ({ onFoodDetail }: Props) => {
// State variables
const [mealTime, setMealTime] = useState<PassioMealTime>('breakfast')
const [loading, setLoading] = useState<boolean>(false)
const [foodResults, setFoodResults] = useState<PassioFoodDataInfo[] | null>()
const mealTimes: PassioMealTime[] = ['breakfast', 'dinner', 'lunch', 'snack']
// Effect for handling debounced search term changes
useEffect(() => {
async function init() {
try {
setLoading(true)
setFoodResults([])
// Fetch food results from the PassioSDK based on the query
const searchFoods = await PassioSDK.fetchSuggestions(mealTime)
setFoodResults(searchFoods)
} catch (error) {
// Handle errors, e.g., network issues or API failures
setFoodResults([])
} finally {
// Reset loading state to indicate the end of the search
setLoading(false)
}
}
init()
}, [mealTime])
const onResultItemPress = useCallback(
async (foodSearchResult: PassioFoodDataInfo) => {
const result = await PassioSDK.fetchFoodItemForDataInfo(foodSearchResult)
if (result) {
onFoodDetail(result)
}
},
[onFoodDetail]
)
const onChangeMeal = (mealTime: PassioMealTime) => {
setMealTime(mealTime)
}
return {
foodResults,
mealTimes,
onChangeMeal,
loading,
onResultItemPress,
mealTime,
}
}
export default useSuggestions
import React from 'react'
import {
ActivityIndicator,
FlatList,
SafeAreaView,
StyleSheet,
Text,
TouchableOpacity,
Image,
View,
} from 'react-native'
import {
PassioIconView,
IconSize,
PassioFoodDataInfo,
PassioFoodItem,
PassioMealTime,
} from '@passiolife/nutritionai-react-native-sdk-v3'
import useSuggestions from './useSuggestion'
export interface Props {
onClose: () => void
onFoodDetail: (passioFoodItem: PassioFoodItem) => void
}
export const FoodSuggestion = (props: Props) => {
const styles = suggestionStyle()
// Destructure values from the custom hook
const {
foodResults,
loading,
onResultItemPress,
mealTime,
onChangeMeal,
mealTimes,
} = useSuggestions(props)
// Function to render each item in the FlatList
const renderSuggestionItem = ({ item }: { item: PassioFoodDataInfo }) => {
return (
<TouchableOpacity
style={styles.itemContainer}
onPress={() => onResultItemPress(item)}
>
<View style={styles.itemIconContainer}>
<PassioIconView
style={styles.itemIcon}
config={{
passioID: item.iconID,
iconSize: IconSize.PX360,
}}
/>
</View>
<View>
<Text style={styles.itemFoodName}>{item.foodName}</Text>
<Text style={styles.itemBrandName}>
{Math.round(item.nutritionPreview?.calories ?? 0) + ' kcal'}
{item.brandName ? ', ' + item.brandName : ''}
</Text>
</View>
</TouchableOpacity>
)
}
const renderMealTime = ({ item }: { item: PassioMealTime }) => {
return (
<TouchableOpacity
style={styles.mealTimeContainer}
onPress={() => onChangeMeal(item)}
>
<Text
style={[
styles.mealTime,
item === mealTime && styles.selectedMealTime,
]}
>
{item}
</Text>
</TouchableOpacity>
)
}
// Display loading indicator when results are empty and loading is true
const renderLoading = () => {
return (
<>{loading ? <ActivityIndicator style={{ marginTop: 100 }} /> : null}</>
)
}
// Render the component
return (
<SafeAreaView style={styles.body}>
<View style={styles.closeButton}>
<TouchableOpacity onPress={props.onClose}>
<Image
style={styles.closeText}
source={require('../assets/back.png')}
/>
</TouchableOpacity>
</View>
<FlatList
data={foodResults}
contentContainerStyle={styles.list}
renderItem={renderSuggestionItem}
ListEmptyComponent={renderLoading}
ListHeaderComponent={() => {
return (
<FlatList data={mealTimes} renderItem={renderMealTime} horizontal />
)
}}
keyExtractor={(item, index) => item.iconID.toString() + index}
/>
</SafeAreaView>
)
}
// Styles for the component
const suggestionStyle = () =>
StyleSheet.create({
closeButton: {},
list: {
marginTop: 16,
},
closeText: {
margin: 16,
height: 24,
width: 24,
},
itemContainer: {
padding: 12,
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
backgroundColor: 'white',
flexDirection: 'row',
borderRadius: 24,
},
mealTimeContainer: {
marginStart: 16,
backgroundColor: 'white',
flexDirection: 'row',
borderRadius: 24,
marginBottom: 16,
overflow: 'hidden',
},
mealTime: {
textTransform: 'capitalize',
paddingVertical: 12,
paddingHorizontal: 12,
fontSize: 16,
},
itemFoodName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 16,
},
selectedMealTime: {
color: 'white',
backgroundColor: 'blue',
},
itemBrandName: {
flex: 1,
textTransform: 'capitalize',
marginHorizontal: 8,
fontSize: 12,
},
itemAlternativeContainer: {
overflow: 'hidden',
},
alternativeContainer: {
marginStart: 16,
alignItems: 'center',
overflow: 'hidden',
alignSelf: 'center',
backgroundColor: 'rgba(238, 242, 255, 1)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 0.1 },
shadowOpacity: 0.5,
shadowRadius: 0.5,
marginVertical: 2,
marginBottom: 14,
elevation: 5,
borderRadius: 24,
},
itemAlternativeName: {
textTransform: 'capitalize',
paddingVertical: 8,
paddingHorizontal: 16,
},
itemIconContainer: {
height: 46,
width: 46,
borderRadius: 30,
overflow: 'hidden',
},
itemIcon: {
height: 46,
width: 46,
},
textInput: {
backgroundColor: 'white',
paddingHorizontal: 16,
padding: 12,
color: 'black',
fontWeight: '500',
fontSize: 16,
marginHorizontal: 16,
},
body: {
backgroundColor: 'rgba(242, 245, 251, 1)',
flex: 1,
},
})
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',
}