diff --git a/example/ios/NutritionUxExample.xcodeproj/xcshareddata/xcschemes/NutritionUxExample.xcscheme b/example/ios/NutritionUxExample.xcodeproj/xcshareddata/xcschemes/NutritionUxExample.xcscheme index 4042878..30f6f00 100644 --- a/example/ios/NutritionUxExample.xcodeproj/xcshareddata/xcschemes/NutritionUxExample.xcscheme +++ b/example/ios/NutritionUxExample.xcodeproj/xcshareddata/xcschemes/NutritionUxExample.xcscheme @@ -50,6 +50,8 @@ ReferencedContainer = "container:NutritionUxExample.xcodeproj"> + + void; + isColum?: boolean; + isCenter?: boolean; +} +export interface FiledSelectionViewRef { + value: () => string | undefined; + errorCheck: () => boolean | undefined; +} + +export const FiledSelectionView = React.forwardRef< + FiledSelectionViewRef, + Props +>( + ( + { + name, + lists, + labelList, + label, + onChange, + value: defaultValue, + isColum = false, + isCenter = false, + }: Props, + ref: React.Ref + ) => { + const branding = useBranding(); + + const styles = requireNutritionFactStyle(branding); + const [value, setValue] = useState(defaultValue); + const [error, setError] = useState(); + + useImperativeHandle( + ref, + () => ({ + value: () => { + return value; + }, + errorCheck: () => { + if (value === undefined || value?.length === 0) { + setError('please enter value'); + } else { + setError(undefined); + } + return value?.length === 0; + }, + }), + [value] + ); + + const renderFiled = () => { + return ( + + + {name} + + { + onChange?.(item); + setError(undefined); + setValue(item); + }} + lists={lists ?? []} + label={label} + labelList={labelList} + style={styles.pickerTextInput} + error={error ?? ''} + /> + + ); + }; + + return {renderFiled()}; + } +); + +const requireNutritionFactStyle = ({}: Branding) => + StyleSheet.create({ + formRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10, + }, + formColum: { + flexDirection: 'column', + justifyContent: 'space-between', + marginBottom: 10, + }, + label: { + flex: 1, + }, + labelMargin: { + marginTop: 10, + }, + pickerTextInput: { + flexWrap: 'wrap', + textAlign: 'right', + }, + }); diff --git a/src/components/filed/FiledView.tsx b/src/components/filed/FiledView.tsx new file mode 100644 index 0000000..c182f24 --- /dev/null +++ b/src/components/filed/FiledView.tsx @@ -0,0 +1,138 @@ +import React, { useImperativeHandle, useState } from 'react'; +import { Text, TextInput } from '..'; +import { + Image, + KeyboardTypeOptions, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; +import { Branding, useBranding } from '../../contexts'; +import { scaleHeight } from '../../utils'; +import { ICONS } from '../../assets'; + +interface Props { + name: string; + value?: string; + label?: string; + keyboardType?: KeyboardTypeOptions; + isColum?: boolean; + onDelete?: () => void; +} + +export interface FiledViewRef { + value: () => string | undefined; + errorCheck: () => boolean | undefined; +} + +export const FiledView = React.forwardRef( + ( + { + name, + value: defaultValue, + keyboardType = 'decimal-pad', + label = 'value', + isColum = false, + onDelete, + }: Props, + ref: React.Ref + ) => { + const branding = useBranding(); + const [value, setValue] = useState(defaultValue); + const [error, setError] = useState(); + + const styles = requireNutritionFactStyle(branding); + + useImperativeHandle( + ref, + () => ({ + value: () => { + return value; + }, + errorCheck: () => { + if (value === undefined || value?.length === 0) { + setError('please enter value'); + } else { + setError(undefined); + } + return value?.length === 0; + }, + }), + [value] + ); + + const renderFiled = () => { + return ( + + + {name} + + { + setValue(text); + setError(undefined); + }} + value={defaultValue} + containerStyle={styles.containerTextInput} + placeholder={label} + error={error} + enterKeyHint="next" + keyboardType={keyboardType} + /> + {onDelete && ( + + + + )} + + ); + }; + + return {renderFiled()}; + } +); + +const requireNutritionFactStyle = ({ white, gray300 }: Branding) => + StyleSheet.create({ + formRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 10, + }, + formColum: { + flexDirection: 'column', + justifyContent: 'space-between', + marginBottom: 10, + }, + label: { + flex: 1, + }, + labelMargin: { + marginTop: 10, + }, + delete: { + height: 24, + width: 24, + marginHorizontal: 6, + }, + textInput: { + textAlign: 'left', + flex: 1, + fontWeight: '400', + fontSize: 14, + backgroundColor: white, + borderColor: gray300, + paddingVertical: scaleHeight(8), + borderRadius: scaleHeight(4), + }, + containerTextInput: { + flex: 1, + }, + }); diff --git a/src/components/filed/index.ts b/src/components/filed/index.ts new file mode 100644 index 0000000..3f64981 --- /dev/null +++ b/src/components/filed/index.ts @@ -0,0 +1,2 @@ +export * from './FiledSelectionView'; +export * from './FiledView'; diff --git a/src/components/index.ts b/src/components/index.ts index 356ce32..ec6124e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,6 +41,8 @@ export * from './weeklyAddhernce'; export * from './progressBard'; export * from './timeStamp'; export * from './mealPlan'; +export * from './tab'; +export * from './filed'; import ActionSheet from './actionSheets/ActionSheet'; export { ActionSheet }; diff --git a/src/components/listPickers/ListPicker.tsx b/src/components/listPickers/ListPicker.tsx index 399ca81..5fad0e1 100644 --- a/src/components/listPickers/ListPicker.tsx +++ b/src/components/listPickers/ListPicker.tsx @@ -18,11 +18,12 @@ interface Props { labelList?: string[]; value: string; title: string; - error: string; + error?: string; style: StyleProp; label?: string; extraWidth?: number; onChange: (size: T) => void; + isCenter?: boolean; } export const ListPicker: React.FC> = ({ @@ -32,6 +33,8 @@ export const ListPicker: React.FC> = ({ extraWidth, value, onChange, + isCenter, + error, }) => { const branding = useBranding(); const styles = menuStyle(branding); @@ -49,6 +52,8 @@ export const ListPicker: React.FC> = ({ }} > > = ({ return ( > = ({ } > - {label ?? value} + + {label ?? value} + + {error && ( + + {error} + + )} ); }; diff --git a/src/components/listPickers/menu.styles.tsx b/src/components/listPickers/menu.styles.tsx index 95d4c7a..6a19ad5 100644 --- a/src/components/listPickers/menu.styles.tsx +++ b/src/components/listPickers/menu.styles.tsx @@ -2,11 +2,11 @@ import { StyleSheet } from 'react-native'; import type { Branding } from '../../contexts'; import { scaleHeight, scaleWidth } from '../../utils'; -const menuStyle = ({ white, border }: Branding) => +const menuStyle = ({ white, gray300, border, error }: Branding) => StyleSheet.create({ main: { backgroundColor: white, - borderColor: border, + borderColor: gray300, flexDirection: 'row', borderRadius: 4, paddingVertical: 8, @@ -17,13 +17,16 @@ const menuStyle = ({ white, border }: Branding) => mainTitle: { flex: 1, marginStart: 12, - textTransform: 'capitalize', }, icon: { marginEnd: 12, height: scaleHeight(20), width: scaleWidth(20), }, + error: { + color: error, + }, + optionContainer: { backgroundColor: white, paddingVertical: scaleHeight(16), @@ -33,7 +36,6 @@ const menuStyle = ({ white, border }: Branding) => }, optionTitle: { marginStart: scaleWidth(12), - textTransform: 'capitalize', }, optionIcon: { height: scaleHeight(20), diff --git a/src/components/logOptions/LogOptions.tsx b/src/components/logOptions/LogOptions.tsx index ea92f92..418ee3f 100644 --- a/src/components/logOptions/LogOptions.tsx +++ b/src/components/logOptions/LogOptions.tsx @@ -13,6 +13,7 @@ interface Props { onTakePicture: () => void; onTakeCamera: () => void; onAiAdvisor: () => void; + onMyFoods: () => void; } type Type = 'All' | 'UseImage'; @@ -45,6 +46,7 @@ export const LogOptions = ({ {type === 'All' ? ( <> + {/* {renderItem(ICONS.logOptionFavorite, 'My Foods', onMyFoods)} */} {renderItem(ICONS.logOptionFavorite, 'Favorites', onFavorite)} {renderItem(ICONS.Mic, 'Voice Logging', onVoiceLogging)} {renderItem(ICONS.AIAdvisor, 'AI Advisor', onAiAdvisor)} diff --git a/src/components/picker/Picker.tsx b/src/components/picker/Picker.tsx index 5a998c9..af7c92e 100644 --- a/src/components/picker/Picker.tsx +++ b/src/components/picker/Picker.tsx @@ -3,13 +3,14 @@ import { View, Modal, Pressable, Platform } from 'react-native'; import { useBranding } from '../../contexts'; import tutorialStyle from './picker.styles'; import { Card } from '../cards'; -import { scaleWidth } from '../../utils'; +import { scaleWidth, screenHeight } from '../../utils'; interface PickerProps extends React.PropsWithChildren { top?: boolean; bottom?: boolean; options: React.JSX.Element; extraWidth?: number; + isCenter?: boolean; } export interface PickerRef { onClose: () => void; @@ -46,7 +47,7 @@ export const Picker = React.forwardRef( (fx: number, fy: number, width: number, height: number) => { const newMeasure: Matrix = { x: fx, - y: fy, + y: props.isCenter ? screenHeight / 2 : fy, width: width, height: height, }; diff --git a/src/components/tab/TabBar.tsx b/src/components/tab/TabBar.tsx new file mode 100644 index 0000000..228b399 --- /dev/null +++ b/src/components/tab/TabBar.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { Text } from '../../components'; +import { Branding, useBranding } from '../../contexts'; +import { scaleHeight } from '../../utils'; + +export interface TabBarProps { + list: string[]; + onTabSelect: (value: string) => void; +} + +export const TabBar = ({ list, onTabSelect }: TabBarProps) => { + const styles = tabStyles(useBranding()); + const [tab, setTab] = useState(list[0]); + + return ( + + + { + setTab(list[0]); + onTabSelect(list[0]); + }} + > + + {list[0]} + + + { + setTab(list[1]); + onTabSelect(list[1]); + }} + > + + {list[1]} + + + + + + + + + ); +}; + +const tabStyles = ({ primaryColor, text }: Branding) => + StyleSheet.create({ + tabContainer: { + flexDirection: 'row', + paddingVertical: scaleHeight(12), + }, + touchableTab: { + flex: 1, + }, + tabTex: { + fontSize: 18, + fontWeight: '400', + textAlign: 'center', + color: text, + }, + tabSelectText: { + fontWeight: '600', + color: primaryColor, + }, + lineContainer: { + flexDirection: 'row', + }, + tabLine: { + height: 2, + borderRadius: 24, + flex: 1, + }, + tabSelectLine: { + backgroundColor: primaryColor, + }, + }); diff --git a/src/components/tab/index.ts b/src/components/tab/index.ts new file mode 100644 index 0000000..832e14a --- /dev/null +++ b/src/components/tab/index.ts @@ -0,0 +1 @@ +export * from './TabBar'; diff --git a/src/constants/colors.ts b/src/constants/colors.ts index a4d66c4..a077578 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -20,7 +20,7 @@ export const COLORS = { grayscale: '#6E7191', grayscaleAsh: '#262338', grayscaleBody: '#4E4B66', - grayscaleLine: '#D9DBE9', + grayscaleLine: '#D1D5DB', grayscaleInput: '#EFF0F6', grey9: '#D0D3DA', monochrome: '#6E7191', diff --git a/src/navigaitons/BottomTabNavigations.tsx b/src/navigaitons/BottomTabNavigations.tsx index 39db9ec..ce0f537 100644 --- a/src/navigaitons/BottomTabNavigations.tsx +++ b/src/navigaitons/BottomTabNavigations.tsx @@ -23,6 +23,7 @@ export interface TabBarProps extends BottomTabBarProps { onTakePicture: () => void; onTakeCamera: () => void; onAiAdvisor: () => void; + onMyFoods: () => void; } export const renderTabBarIcons = ( diff --git a/src/navigaitons/HomeBottomNavigations.tsx b/src/navigaitons/HomeBottomNavigations.tsx index 64c52fa..bcea557 100644 --- a/src/navigaitons/HomeBottomNavigations.tsx +++ b/src/navigaitons/HomeBottomNavigations.tsx @@ -144,6 +144,12 @@ export const HomeBottomNavigation = React.memo(() => { logToMeal: undefined, }); }} + onMyFoods={() => { + navigation.navigate('MyFoodsScreen', { + logToDate: mealLogDateRef.current, + logToMeal: undefined, + }); + }} onTakePicture={onTakePicture} {...props} items={menu} diff --git a/src/navigaitons/Nutrition-Navigator.tsx b/src/navigaitons/Nutrition-Navigator.tsx index dcfec6f..a9fd3f7 100644 --- a/src/navigaitons/Nutrition-Navigator.tsx +++ b/src/navigaitons/Nutrition-Navigator.tsx @@ -19,6 +19,8 @@ import { TakePictureScreen, AdvisorScreen, ImagePickerScreen, + MyFoodsScreen, + FoodCreatorScreen, } from '../screens'; import { DashboardScreenRoute, @@ -44,6 +46,8 @@ import { TakePictureScreenRoute, AdvisorScreenRoute, ImagePickerScreenRoute, + MyFoodsScreenRoute, + FoodCreatorScreenRoute, } from './Route'; import MyPlanScreen from '../screens/myPlans/MyPlanScreen'; import { HomeBottomNavigation } from './HomeBottomNavigations'; @@ -201,6 +205,16 @@ export const NutritionNavigator = () => { name={ImagePickerScreenRoute} component={ImagePickerScreen} /> + + { floatingRef.current?.onClose(); props.onAiAdvisor(); }} + onMyFoods={() => { + floatingRef.current?.onClose(); + props.onMyFoods(); + }} /> } /> diff --git a/src/navigaitons/params/NutritionNavigatorParam.ts b/src/navigaitons/params/NutritionNavigatorParam.ts index c0f4a90..0229789 100644 --- a/src/navigaitons/params/NutritionNavigatorParam.ts +++ b/src/navigaitons/params/NutritionNavigatorParam.ts @@ -60,6 +60,14 @@ export interface AdvisorScreenProps { logToDate?: Date | undefined; logToMeal?: MealLabel | undefined; } +export interface FoodCreatorNavProps { + logToDate?: Date | undefined; + logToMeal?: MealLabel | undefined; +} +export interface MyFoodsScreenNavProps { + logToDate?: Date | undefined; + logToMeal?: MealLabel | undefined; +} export type ParamList = { MealLogScreen: MealLogScreenProps; @@ -89,4 +97,6 @@ export type ParamList = { TakePictureScreen: TakePictureScreenProps; AdvisorScreen: AdvisorScreenProps; ImagePickerScreen: ImagePickerProps; + FoodCreatorScreen: FoodCreatorNavProps; + MyFoodsScreen: MyFoodsScreenNavProps; }; diff --git a/src/screens/advisor/AdvisorScreen.tsx b/src/screens/advisor/AdvisorScreen.tsx index 996f961..1c80766 100644 --- a/src/screens/advisor/AdvisorScreen.tsx +++ b/src/screens/advisor/AdvisorScreen.tsx @@ -110,6 +110,7 @@ export const AdvisorScreen = () => { keyExtractor={(_item, index) => index.toString()} renderItem={renderItem} showsVerticalScrollIndicator={false} + contentContainerStyle={{ flexGrow: 1 }} style={styles.flatListStyle} // onContentSizeChange={() => listRef.current?.scrollToEnd()} // onLayout={() => listRef.current?.scrollToEnd()} diff --git a/src/screens/advisor/view/BottomBar.tsx b/src/screens/advisor/view/BottomBar.tsx index ec5862d..6daa06f 100644 --- a/src/screens/advisor/view/BottomBar.tsx +++ b/src/screens/advisor/view/BottomBar.tsx @@ -35,44 +35,55 @@ export const BottomBar = ({ return ( - {isOptionShow ? ( - - - - + {inputValue.length === 0 && ( + <> + {isOptionShow ? ( + + + + - - - - - ) : ( - - - + + + + + ) : ( + + + + )} + )} - {renderText()} + + + {renderText()} + {tools && tools?.length > 0 && ( )} - + ); }; @@ -98,7 +105,6 @@ const ResponseViewStyle = () => }, receivedMsgView: { backgroundColor: '#6366F1', - alignSelf: 'flex-start', borderBottomRightRadius: 8, borderBottomLeftRadius: 0, }, diff --git a/src/screens/foodCreator/FoodCreator.styles.ts b/src/screens/foodCreator/FoodCreator.styles.ts new file mode 100644 index 0000000..ba70ec2 --- /dev/null +++ b/src/screens/foodCreator/FoodCreator.styles.ts @@ -0,0 +1,15 @@ +import { StyleSheet } from 'react-native'; +import { getStatusBarHeight } from 'react-native-status-bar-height'; +import type { Branding } from '../../contexts'; + +export const foodCreatorStyle = ({ searchBody }: Branding) => + StyleSheet.create({ + statusBarLayout: { + height: getStatusBarHeight(), + backgroundColor: searchBody, + }, + body: { + backgroundColor: searchBody, + flex: 1, + }, + }); diff --git a/src/screens/foodCreator/FoodCreatorScreen.tsx b/src/screens/foodCreator/FoodCreatorScreen.tsx new file mode 100644 index 0000000..73b287c --- /dev/null +++ b/src/screens/foodCreator/FoodCreatorScreen.tsx @@ -0,0 +1,63 @@ +import { ScrollView, View } from 'react-native'; + +import React from 'react'; +import { foodCreatorStyle } from './FoodCreator.styles'; +import { useFoodCreator } from './useFoodCreator'; +import { BackNavigation, BasicButton } from '../../components'; +import { FoodCreatorFoodDetail } from './views/FoodCreatorFoodDetail'; +import { RequireNutritionFacts } from './views/RequireNutritionFacts'; +import { OtherNutritionFacts } from './views/OtherNutritionFacts'; + +export const FoodCreatorScreen = () => { + const { + branding, + otherNutritionFactsRef, + requireNutritionFactsRef, + foodCreatorFoodDetailRef, + onSavePress, + } = useFoodCreator(); + + const styles = foodCreatorStyle(branding); + + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/src/screens/foodCreator/data.ts b/src/screens/foodCreator/data.ts new file mode 100644 index 0000000..1316467 --- /dev/null +++ b/src/screens/foodCreator/data.ts @@ -0,0 +1,34 @@ +import type { NutrientType } from '../../models'; + +export const Units = [ + 'Servings', + 'Piece', + 'cup', + 'oz', + 'g', + 'ml', + 'handful', + 'scoop', + 'tbsp', + 'tsp', + 'slice', + 'can', + 'bottle', + 'bar', + 'packet', +]; + +export const OtherNutrients: NutrientType[] = [ + 'satFat', + 'transFat', + 'cholesterol', + 'sodium', + 'fiber', + 'sugars', + 'sugarAdded', + 'vitaminD', + 'calcium', + 'iron', + 'potassium', +]; +export const Weights = ['grams', 'ml']; diff --git a/src/screens/foodCreator/index.tsx b/src/screens/foodCreator/index.tsx new file mode 100644 index 0000000..df80ca5 --- /dev/null +++ b/src/screens/foodCreator/index.tsx @@ -0,0 +1 @@ +export * from './FoodCreatorScreen'; diff --git a/src/screens/foodCreator/useFoodCreator.ts b/src/screens/foodCreator/useFoodCreator.ts new file mode 100644 index 0000000..8f8bf77 --- /dev/null +++ b/src/screens/foodCreator/useFoodCreator.ts @@ -0,0 +1,36 @@ +import { useRef } from 'react'; +import { useBranding } from '../../contexts'; +import type { OtherNutritionFactsRef } from './views/OtherNutritionFacts'; +import type { RequireNutritionFactsRef } from './views/RequireNutritionFacts'; +import type { FoodCreatorFoodDetailRef } from './views/FoodCreatorFoodDetail'; + +export const useFoodCreator = () => { + const branding = useBranding(); + const otherNutritionFactsRef = useRef(null); + const requireNutritionFactsRef = useRef(null); + const foodCreatorFoodDetailRef = useRef(null); + + const onSavePress = () => { + const info = foodCreatorFoodDetailRef.current?.getValue(); + const requireNutritionFact = requireNutritionFactsRef.current?.getValue(); + const otherNutritionFact = otherNutritionFactsRef.current?.getValue(); + + if (info?.isNotValid) { + return; + } + if (requireNutritionFact?.isNotValid) { + return; + } + if (otherNutritionFact?.isNotValid) { + return; + } + }; + + return { + branding, + otherNutritionFactsRef, + requireNutritionFactsRef, + foodCreatorFoodDetailRef, + onSavePress, + }; +}; diff --git a/src/screens/foodCreator/views/FoodCreatorFoodDetail.tsx b/src/screens/foodCreator/views/FoodCreatorFoodDetail.tsx new file mode 100644 index 0000000..8715dbc --- /dev/null +++ b/src/screens/foodCreator/views/FoodCreatorFoodDetail.tsx @@ -0,0 +1,151 @@ +import React, { useImperativeHandle, useMemo, useRef } from 'react'; +import { Card, Text } from '../../../components'; +import { Image, StyleSheet, View } from 'react-native'; +import { Branding, useBranding } from '../../../contexts'; +import { FiledView, FiledViewRef } from '../../../components/filed/FiledView'; +import { + FiledSelectionView, + FiledSelectionViewRef, +} from '../../../components/filed/FiledSelectionView'; +import { Units } from '../data'; +import { ICONS } from '../../../assets'; + +interface Props {} + +export type FoodCreatorFoodDetailType = 'name' | 'brand' | 'barcode'; + +interface Value { + records: Record; + isNotValid?: boolean; +} + +export interface FoodCreatorFoodDetailRef { + getValue: () => Value; +} + +export const FoodCreatorFoodDetail = React.forwardRef< + FoodCreatorFoodDetailRef, + Props +>(({}: Props, ref: React.Ref) => { + const branding = useBranding(); + + const styles = requireNutritionFactStyle(branding); + + const nameRef = useRef(null); + const brandNameRef = useRef(null); + const barcodeRef = useRef(null); + + const refs = useMemo( + () => ({ + name: nameRef, + brand: brandNameRef, + barcode: barcodeRef, + }), + [] + ); + + useImperativeHandle( + ref, + () => ({ + getValue: () => { + let record: Record = {} as Record< + FoodCreatorFoodDetailType, + string + >; + let isNotValid = false; + + (Object.keys(refs) as FoodCreatorFoodDetailType[]).forEach((key) => { + const currentRef = refs[key].current; + const value = currentRef?.value(); + currentRef?.errorCheck(); + if (value === undefined || value.length === 0) { + isNotValid = true; + } + record[key] = value ?? ''; + }); + + return { + records: record, + isNotValid, + }; + }, + }), + [refs] + ); + + return ( + + { + + {'Scan Description'} + + + + + {'Edit Image'} + + + + + + + + + + } + + ); +}); + +const requireNutritionFactStyle = ({}: Branding) => + StyleSheet.create({ + card: { + marginHorizontal: 16, + marginVertical: 16, + padding: 16, + }, + title: { + marginBottom: 16, + }, + container: { + flexDirection: 'row', + alignContent: 'space-around', + justifyContent: 'space-between', + }, + left: { + flex: 1, + alignContent: 'center', + alignItems: 'center', + alignSelf: 'center', + }, + right: { + flex: 1.5, + }, + editImage: { + marginVertical: 4, + fontSize: 10, + }, + icon: { + height: 80, + width: 80, + alignItems: 'center', + alignSelf: 'center', + justifyContent: 'center', + alignContent: 'center', + }, + }); diff --git a/src/screens/foodCreator/views/OtherNutritionFacts.tsx b/src/screens/foodCreator/views/OtherNutritionFacts.tsx new file mode 100644 index 0000000..8328df4 --- /dev/null +++ b/src/screens/foodCreator/views/OtherNutritionFacts.tsx @@ -0,0 +1,130 @@ +import React, { useImperativeHandle, useRef, useState } from 'react'; +import { Card, Text } from '../../../components'; +import { StyleSheet, View } from 'react-native'; +import { Branding, useBranding } from '../../../contexts'; +import { FiledView, FiledViewRef } from '../../../components/filed/FiledView'; +import { FiledSelectionView } from '../../../components/filed/FiledSelectionView'; +import { OtherNutrients } from '../data'; +import { FlatList } from 'react-native'; +import { nutrientName, type NutrientType } from '../../../models'; + +interface Props {} + +interface Value { + records: Record; + isNotValid?: boolean; +} +export interface OtherNutritionFactsRef { + getValue: () => Value; +} + +export const OtherNutritionFacts = React.forwardRef< + OtherNutritionFactsRef, + Props +>(({}: Props, ref: React.Ref) => { + const branding = useBranding(); + + const styles = requireNutritionFactStyle(branding); + const [defaultList, setDefaultList] = + useState(OtherNutrients); + const [list, setList] = useState([]); + const labelList = defaultList.map((i) => { + return nutrientName[i].toString() ?? ''; + }); + + const refs = useRef>>({}); + + useImperativeHandle( + ref, + () => ({ + getValue: () => { + let record: Record = {} as Record< + NutrientType, + string + >; + let isNotValid = false; + list.forEach((item) => { + const sleetedRef = refs.current[item]; + if (sleetedRef && sleetedRef.current) { + if (sleetedRef.current.errorCheck()) { + isNotValid = true; + } + record[item as NutrientType] = sleetedRef.current.value() ?? ''; + } + }); + + return { + records: record, + isNotValid: isNotValid, + }; + }, + }), + [list] + ); + + // Initialize refs for each item in the list + list.forEach((item) => { + if (!refs.current[item]) { + refs.current[item] = React.createRef(); + } + }); + + return ( + + { + + {'Other Nutrition Facts'} + item} + renderItem={({ item }) => { + return ( + { + setList((i) => [...i.filter((o) => item !== o)]); + setDefaultList((i) => [...i, item as NutrientType]); + }} + /> + ); + }} + /> + {defaultList.length > 0 && ( + { + setList((i) => [...i, item]); + setDefaultList((i) => [...i.filter((o) => item !== o)]); + }} + /> + )} + + } + + ); +}); + +const requireNutritionFactStyle = ({}: Branding) => + StyleSheet.create({ + card: { + marginHorizontal: 16, + marginVertical: 16, + padding: 16, + }, + title: { + marginBottom: 16, + }, + container: { + flexDirection: 'row', + alignContent: 'space-around', + justifyContent: 'space-between', + }, + }); diff --git a/src/screens/foodCreator/views/RequireNutritionFacts.tsx b/src/screens/foodCreator/views/RequireNutritionFacts.tsx new file mode 100644 index 0000000..0d5c47c --- /dev/null +++ b/src/screens/foodCreator/views/RequireNutritionFacts.tsx @@ -0,0 +1,124 @@ +import React, { useImperativeHandle, useRef, useState, useMemo } from 'react'; +import { Card, FiledViewRef, Text } from '../../../components'; +import { StyleSheet, View } from 'react-native'; +import { Branding, useBranding } from '../../../contexts'; +import { FiledView } from '../../../components'; +import { + FiledSelectionView, + FiledSelectionViewRef, +} from '../../../components/filed/FiledSelectionView'; +import { Units, Weights } from '../data'; + +interface Props {} + +export type RequireNutritionFactsType = + | 'calories' + | 'ServingSize' + | 'Units' + | 'Weight' + | 'Fat' + | 'Carbs' + | 'Protein'; + +interface Value { + records: Record; + isNotValid?: boolean; +} + +export interface RequireNutritionFactsRef { + getValue: () => Value; +} + +export const RequireNutritionFacts = React.forwardRef< + RequireNutritionFactsRef, + Props +>(({}: Props, ref: React.Ref) => { + const branding = useBranding(); + const styles = requireNutritionFactStyle(branding); + + const [units, setUnits] = useState(''); + + const servingSizeRef = useRef(null); + const caloriesRef = useRef(null); + const fatRef = useRef(null); + const carbsRef = useRef(null); + const proteinRef = useRef(null); + const unitRef = useRef(null); + const weightRef = useRef(null); + + const refs = useMemo( + () => ({ + ServingSize: servingSizeRef, + Units: unitRef, + Weight: weightRef, + calories: caloriesRef, + Fat: fatRef, + Carbs: carbsRef, + Protein: proteinRef, + }), + [] + ); + + useImperativeHandle( + ref, + () => ({ + getValue: () => { + let record: Record = {} as Record< + RequireNutritionFactsType, + string + >; + let isNotValid = false; + + (Object.keys(refs) as RequireNutritionFactsType[]).forEach((key) => { + const currentRef = refs[key].current; + const value = currentRef?.value(); + currentRef?.errorCheck(); + if (value === undefined || value.length === 0) { + isNotValid = true; + } + record[key] = value ?? ''; + }); + + return { + records: record, + isNotValid, + }; + }, + }), + [refs] + ); + + return ( + + + {'Required Nutrition Facts'} + + setUnits(value)} + /> + {units === 'g' || units === 'ml' ? null : ( + + )} + + + + + + + ); +}); + +const requireNutritionFactStyle = ({}: Branding) => + StyleSheet.create({ + card: { + marginHorizontal: 16, + marginVertical: 16, + padding: 16, + }, + title: { + marginBottom: 16, + }, + }); diff --git a/src/screens/index.tsx b/src/screens/index.tsx index a3b32e3..a296d0d 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -21,3 +21,5 @@ export * from './voiceLogging'; export * from './takePicture'; export * from './advisor'; export * from './imagePicker'; +export * from './foodCreator'; +export * from './myFoods'; diff --git a/src/screens/myFoods/MyFoodsScreen.styles.ts b/src/screens/myFoods/MyFoodsScreen.styles.ts new file mode 100644 index 0000000..299e27b --- /dev/null +++ b/src/screens/myFoods/MyFoodsScreen.styles.ts @@ -0,0 +1,22 @@ +import { StyleSheet } from 'react-native'; +import { getStatusBarHeight } from 'react-native-status-bar-height'; +import type { Branding } from '../../contexts'; + +export const myFoodScreenStyle = ({ searchBody }: Branding) => + StyleSheet.create({ + statusBarLayout: { + height: getStatusBarHeight(), + backgroundColor: searchBody, + }, + body: { + backgroundColor: searchBody, + flex: 1, + }, + container: { + flex: 1, + }, + button: { + marginVertical: 24, + marginHorizontal: 16, + }, + }); diff --git a/src/screens/myFoods/MyFoodsScreen.tsx b/src/screens/myFoods/MyFoodsScreen.tsx new file mode 100644 index 0000000..306a2af --- /dev/null +++ b/src/screens/myFoods/MyFoodsScreen.tsx @@ -0,0 +1,47 @@ +import { View } from 'react-native'; + +import React from 'react'; +import { myFoodScreenStyle } from './MyFoodsScreen.styles'; +import { useMyFoodScreen } from './useMyFoodScreen'; +import { BackNavigation, BasicButton, TabBar } from '../../components'; +import { useNavigation } from '@react-navigation/native'; +import type { StackNavigationProp } from '@react-navigation/stack'; +import type { ParamList } from '../../navigaitons'; +import { useSharedValue } from 'react-native-reanimated'; + +type ScreenNavigationProps = StackNavigationProp; + +const TabList = ['Custom Foods', 'Recipe']; + +export const MyFoodsScreen = () => { + const { branding } = useMyFoodScreen(); + const navigation = useNavigation(); + const tab = useSharedValue(TabList[0]); + + const styles = myFoodScreenStyle(branding); + + const renderTab = () => { + return ( + { + tab.value = value; + }} + /> + ); + }; + + return ( + + + + { + navigation.navigate('FoodCreatorScreen', {}); + }} + /> + + ); +}; diff --git a/src/screens/myFoods/index.tsx b/src/screens/myFoods/index.tsx new file mode 100644 index 0000000..25009ff --- /dev/null +++ b/src/screens/myFoods/index.tsx @@ -0,0 +1 @@ +export * from './MyFoodsScreen'; diff --git a/src/screens/myFoods/useMyFoodScreen.ts b/src/screens/myFoods/useMyFoodScreen.ts new file mode 100644 index 0000000..d4cb24d --- /dev/null +++ b/src/screens/myFoods/useMyFoodScreen.ts @@ -0,0 +1,9 @@ +import { useBranding } from '../../contexts'; + +export const useMyFoodScreen = () => { + const branding = useBranding(); + + return { + branding, + }; +};