Integrate Food Search
Passio provide searchForFood API to get FoodSearchResult
Last updated
Passio provide searchForFood API to get FoodSearchResult
Last updated
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',
}