-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implemented front-end for Update screen
- Loading branch information
Showing
2 changed files
with
274 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,270 @@ | ||
export default function Update() { | ||
<></>; | ||
import React, { useEffect, useState } from 'react'; | ||
import { StyleSheet, TextInput } from 'react-native'; | ||
import { Button, Colors, KeyboardAwareScrollView, Text, View } from 'react-native-ui-lib'; | ||
import { Portal } from 'react-native-paper'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { getAllUnits } from 'database/queries/unitsQueries'; | ||
import { SQLiteDatabase, useSQLiteContext } from 'expo-sqlite'; | ||
|
||
import { useColors } from 'context/ColorContext'; | ||
|
||
// components | ||
import Dialogs from 'components/Shared/Dialogs'; | ||
import IconSVG from 'components/Shared/icons/IconSVG'; | ||
import UnitPicker from 'components/Shared/UnitPicker'; | ||
import MacrosBarChart from 'components/Screens/Create/MacrosBarChart'; | ||
import MacroInputField from 'components/Screens/Create/MacroInputField'; | ||
|
||
import { Food, Nutritable, Unit } from 'database/types'; | ||
import { getAllFoodNames } from 'database/queries/foodsQueries'; | ||
|
||
import calculateCalories from 'utils/calculateCalories'; | ||
import { validateFoodInputs } from 'utils/validation/validateFood'; | ||
import { Validation, ValidationStatus } from 'utils/validation/types'; | ||
import { StaticScreenProps } from '@react-navigation/native'; | ||
|
||
type Props = StaticScreenProps<{ | ||
food: Food; | ||
nutritable: Nutritable; | ||
}>; | ||
|
||
// (!!!) This screen produces a warning due to nesting UnitPicker inside a KeyboardAwareScrollView. | ||
// Nevertheless, this is strictly necessary to avoid the keyboard from covering the bottom-most input fields. | ||
export default function Update({ route }: Props) { | ||
const { food, nutritable } = route.params; | ||
|
||
const colors = useColors(); | ||
const database: SQLiteDatabase = useSQLiteContext(); | ||
|
||
// Fetches the names of all existing foods | ||
const { data: names = [], isFetched: namesFetched } = useQuery({ | ||
queryKey: ['allNames'], | ||
queryFn: () => getAllFoodNames(database), | ||
initialData: [], | ||
}); | ||
|
||
// Stateful nutritional data | ||
// (!) Attention: whenever kcals would be used, first check whether it has any input. | ||
// If it doesn't use expectedKcals instead for a smoother user experience. | ||
const [name, setName] = useState<string>(food.name); | ||
const [kcals, setKcals] = useState<string>(nutritable.kcals.toString()); | ||
const [measure, setMeasure] = useState<string>(nutritable.baseMeasure.toString()); | ||
const [fat, setFat] = useState<string>(nutritable.fats.toString()); | ||
const [carbs, setCarbs] = useState<string>(nutritable.carbs.toString()); | ||
const [protein, setProtein] = useState<string>(nutritable.protein.toString()); | ||
|
||
// Calculates expected kcals | ||
const [expectedKcals, setExpectedKcals] = useState(''); | ||
useEffect(() => { | ||
setExpectedKcals( | ||
calculateCalories(Number(protein), Number(carbs), Number(fat)).toFixed(2).toString() | ||
); | ||
}, [protein, carbs, fat]); | ||
|
||
// Initializes the validation status | ||
const [validationAttempted, setValidationAttempted] = useState(false); | ||
const [validation, setValidation] = useState<Validation>({ | ||
status: ValidationStatus.Valid, | ||
errors: [], | ||
}); | ||
|
||
// Resets the validation status | ||
const resetValidation = () => { | ||
setValidationAttempted(false); | ||
setValidation({ | ||
status: ValidationStatus.Valid, | ||
errors: [], | ||
}); | ||
}; | ||
|
||
// Resets the validation status every time a field changes | ||
useEffect(resetValidation, [name, kcals, measure, fat, protein, carbs]); | ||
|
||
// Controls the showing of warnings and errors | ||
const [showDialogs, setShowDialogs] = useState(false); | ||
useEffect(() => { | ||
setShowDialogs(validation.status !== ValidationStatus.Valid); | ||
}, [validation]); | ||
|
||
return ( | ||
<> | ||
{/* Using a portal is needed because the nested scrollViews mess up the Dialog component */} | ||
<Portal.Host> | ||
<Portal> | ||
{/* Show the warnings here component is done */} | ||
<Dialogs show={showDialogs} setShow={setShowDialogs} errors={validation.errors} /> | ||
</Portal> | ||
<KeyboardAwareScrollView | ||
behavior="padding" | ||
nestedScrollEnabled | ||
extraScrollHeight={160} | ||
contentContainerStyle={styles.container}> | ||
<View style={styles.nameField}> | ||
<TextInput | ||
placeholder={name} | ||
onChangeText={(text) => setName(text.length === 0 ? food.name : text)} | ||
placeholderTextColor={Colors.violet40} | ||
style={styles.nameInput} | ||
/> | ||
</View> | ||
{/* Graph */} | ||
<MacrosBarChart fat={Number(fat)} carbs={Number(carbs)} protein={Number(protein)} /> | ||
{/* Macros & Unit */} | ||
<View row gap-20> | ||
{/* Macros */} | ||
<View flex gap-20> | ||
{/* Fat */} | ||
<MacroInputField | ||
text={fat} | ||
onChangeText={(text) => setFat(text)} | ||
color={colors.get('fat')} | ||
unitSymbol={'g'} | ||
iconName={'bacon-solid'} | ||
maxLength={7} | ||
/> | ||
{/* Carbs */} | ||
<MacroInputField | ||
text={carbs} | ||
onChangeText={(text) => setCarbs(text)} | ||
color={colors.get('carbs')} | ||
unitSymbol={'g'} | ||
iconName={'wheat-solid'} | ||
maxLength={7} | ||
/> | ||
{/* Protein */} | ||
<MacroInputField | ||
text={protein} | ||
onChangeText={(text) => setProtein(text)} | ||
color={colors.get('protein')} | ||
unitSymbol={'g'} | ||
iconName={'meat-solid'} | ||
maxLength={7} | ||
/> | ||
</View> | ||
{/* Unit */} | ||
<View style={styles.unitPickerFlex}> | ||
<View style={styles.unitIconBox}> | ||
<IconSVG width={24} name={'ruler-solid'} color={Colors.white} /> | ||
<IconSVG style={styles.unitCaret} color={Colors.violet30} name="caret-down-solid" /> | ||
</View> | ||
<UnitPicker | ||
units={[nutritable.unit]} | ||
onChange={() => { | ||
/* does nothing */ | ||
}} | ||
/> | ||
</View> | ||
</View> | ||
{/* Quantity & Calories */} | ||
<View spread gap-20> | ||
{/* Calories */} | ||
<MacroInputField | ||
text={kcals} | ||
onChangeText={(text) => setKcals(text)} | ||
unitSymbol={'kcal'} | ||
unitIndicatorWidth={60} | ||
iconName={'ball-pile-solid'} | ||
maxLength={9} | ||
placeholder={expectedKcals} | ||
/> | ||
{/* Measure */} | ||
<MacroInputField | ||
text={measure} | ||
onChangeText={(text) => setMeasure(text)} | ||
unitSymbol={nutritable.unit.symbol} | ||
unitIndicatorWidth={60} | ||
iconName={'scale-unbalanced-solid'} | ||
maxLength={7} | ||
/> | ||
</View> | ||
{/* Submit Button */} | ||
<Button | ||
style={{ borderRadius: 12 }} | ||
disabled={validation.status === ValidationStatus.Error} | ||
label={ | ||
validation.status === ValidationStatus.Warning ? 'Proceed anyway' : 'Create food' | ||
} | ||
onPress={() => { | ||
// Validates the current data | ||
const tempValidationStatus: Validation = validateFoodInputs({ | ||
name: name, | ||
existingNames: names.filter((name) => name !== food.name), | ||
kcals: kcals === '' ? expectedKcals : kcals, | ||
expectedKcals, | ||
measure, | ||
}); | ||
// If it is valid OR if it has only warnings but the user still wishes to proceed... | ||
if ( | ||
tempValidationStatus.status === ValidationStatus.Valid || | ||
(tempValidationStatus.status === ValidationStatus.Warning && validationAttempted) | ||
) { | ||
// Creates the nutritable and redirects to the foods list | ||
console.log('nutritable/food updated!'); | ||
} else { | ||
// Makes this validation result available to the rest of the program. | ||
setValidation(tempValidationStatus); | ||
// And signals the validation as attempted (this way we know if the user has already been warned) | ||
setValidationAttempted(true); | ||
} | ||
}} | ||
/> | ||
{/* </View> */} | ||
</KeyboardAwareScrollView> | ||
</Portal.Host> | ||
</> | ||
); | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
nameBox: { | ||
backgroundColor: Colors.violet30, | ||
height: 60, | ||
borderRadius: 20, | ||
justifyContent: 'center', // Center vertically | ||
alignItems: 'center', // Center horizontally | ||
}, | ||
name: { | ||
color: 'white', | ||
fontSize: 20, | ||
textAlign: 'center', | ||
}, | ||
unitPickerFlex: { | ||
flexDirection: 'row', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
height: 160, | ||
width: 128, | ||
gap: 8, | ||
}, | ||
unitIconBox: { | ||
position: 'relative', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
backgroundColor: Colors.violet30, | ||
width: 40, | ||
height: 40, | ||
borderRadius: 10, | ||
}, | ||
unitCaret: { | ||
position: 'absolute', | ||
transform: [{ rotate: '-90deg' }], | ||
right: -20, | ||
zIndex: 1, | ||
}, | ||
nameField: { | ||
backgroundColor: Colors.violet30, | ||
height: 60, | ||
borderRadius: 20, | ||
}, | ||
nameInput: { | ||
flex: 1, | ||
color: 'white', | ||
fontSize: 20, | ||
textAlign: 'center', | ||
paddingHorizontal: 20, | ||
}, | ||
container: { | ||
gap: 20, | ||
padding: 20, | ||
}, | ||
}); |