3 Commits

Author SHA1 Message Date
2e45261354 feat: add camera capture for trip and item images
All checks were successful
Luggage List Build / build-web (push) Successful in 31s
Luggage List Build / build-android (push) Successful in 6m24s
Luggage List Build / release (push) Successful in 14s
2026-04-18 14:19:48 +02:00
bd500674a0 fix(ui): stabilize modal keyboard behavior
All checks were successful
Luggage List Build / build-web (push) Successful in 55s
Luggage List Build / build-android (push) Successful in 6m28s
Luggage List Build / release (push) Successful in 10s
2026-04-18 14:12:34 +02:00
ef7e0ba7a1 style: center and enlarge modals to overlay navbar
All checks were successful
Luggage List Build / build-web (push) Successful in 30s
Luggage List Build / build-android (push) Successful in 6m13s
Luggage List Build / release (push) Successful in 15s
2026-04-18 13:34:42 +02:00
7 changed files with 77 additions and 17 deletions

View File

@@ -37,3 +37,5 @@ Improving & Fixing Bugs (V3)
- [x] Increased safe top inset to avoid status-bar overlap - [x] Increased safe top inset to avoid status-bar overlap
- [x] Added check-up stats (correct/bad/pending) and persisted per snapshot - [x] Added check-up stats (correct/bad/pending) and persisted per snapshot
- [x] Extra UI polish pass (spacing, cards, hierarchy) - [x] Extra UI polish pass (spacing, cards, hierarchy)
- [x] Centered and enlarged edit/check-up modals to fully overlay nav
- [x] Fixed modal keyboard glitching (stable centered keyboard-aware layout)

View File

@@ -34,6 +34,15 @@
"eas": { "eas": {
"projectId": "1275f90e-33c6-4af1-942e-ca29a309f8c8" "projectId": "1275f90e-33c6-4af1-942e-ca29a309f8c8"
} }
} },
"plugins": [
[
"expo-image-picker",
{
"photosPermission": "Allow Luggage List to access your photos for trip and item images.",
"cameraPermission": "Allow Luggage List to use your camera to take trip and item photos."
}
]
]
} }
} }

View File

@@ -169,6 +169,23 @@ export default function AppRoot() {
} }
} }
async function takeImage(onPicked) {
const perm = await ImagePicker.requestCameraPermissionsAsync();
if (!perm.granted) {
Alert.alert('Permission needed', 'Allow camera access to take photos.');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: false,
quality: 0.85,
});
if (!result.canceled && result.assets?.[0]?.uri) {
onPicked(result.assets[0].uri);
}
}
function createTrip() { function createTrip() {
if (!tripForm.name.trim()) { if (!tripForm.name.trim()) {
Alert.alert('Missing name', 'Trip name is required.'); Alert.alert('Missing name', 'Trip name is required.');
@@ -526,6 +543,7 @@ export default function AppRoot() {
tripForm={tripForm} tripForm={tripForm}
updateTripForm={updateTripForm} updateTripForm={updateTripForm}
pickTripImage={() => pickImage((uri) => updateTripForm('imageUri', uri))} pickTripImage={() => pickImage((uri) => updateTripForm('imageUri', uri))}
takeTripImage={() => takeImage((uri) => updateTripForm('imageUri', uri))}
templateTrip={templateTrip} templateTrip={templateTrip}
createTrip={createTrip} createTrip={createTrip}
trips={data.trips} trips={data.trips}
@@ -587,6 +605,7 @@ export default function AppRoot() {
setItemModalVisible={setItemModalVisible} setItemModalVisible={setItemModalVisible}
updateItemForm={updateItemForm} updateItemForm={updateItemForm}
pickItemImage={() => pickImage((uri) => updateItemForm('imageUri', uri))} pickItemImage={() => pickImage((uri) => updateItemForm('imageUri', uri))}
takeItemImage={() => takeImage((uri) => updateItemForm('imageUri', uri))}
saveItemFromModal={saveItemFromModal} saveItemFromModal={saveItemFromModal}
/> />

View File

@@ -15,7 +15,7 @@ export default function CheckupFixModal({
return ( return (
<Modal visible={visible} animationType="slide" transparent> <Modal visible={visible} animationType="slide" transparent>
<View style={styles.modalBackdrop}> <View style={styles.modalBackdrop}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalKeyboardWrap}> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.modalKeyboardWrap}>
<View style={styles.modalCard}> <View style={styles.modalCard}>
<View style={styles.sectionRow}> <View style={styles.sectionRow}>
<Text style={styles.sectionTitle}>Update for this Check-Up</Text> <Text style={styles.sectionTitle}>Update for this Check-Up</Text>
@@ -24,7 +24,12 @@ export default function CheckupFixModal({
</Pressable> </Pressable>
</View> </View>
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}> <ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
contentContainerStyle={{ paddingBottom: 12 }}
showsVerticalScrollIndicator={false}
>
<Field label="Status"> <Field label="Status">
<ChipGroup <ChipGroup
options={ITEM_STATUSES} options={ITEM_STATUSES}

View File

@@ -11,12 +11,13 @@ export default function ItemModal({
setItemModalVisible, setItemModalVisible,
updateItemForm, updateItemForm,
pickItemImage, pickItemImage,
takeItemImage,
saveItemFromModal, saveItemFromModal,
}) { }) {
return ( return (
<Modal visible={visible} animationType="slide" transparent> <Modal visible={visible} animationType="slide" transparent>
<View style={styles.modalBackdrop}> <View style={styles.modalBackdrop}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalKeyboardWrap}> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.modalKeyboardWrap}>
<View style={styles.modalCard}> <View style={styles.modalCard}>
<View style={styles.sectionRow}> <View style={styles.sectionRow}>
<Text style={styles.sectionTitle}>{itemForm.id ? 'Update Item' : 'Add Item'}</Text> <Text style={styles.sectionTitle}>{itemForm.id ? 'Update Item' : 'Add Item'}</Text>
@@ -25,7 +26,12 @@ export default function ItemModal({
</Pressable> </Pressable>
</View> </View>
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}> <ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
contentContainerStyle={{ paddingBottom: 12 }}
showsVerticalScrollIndicator={false}
>
<Field label="Name"> <Field label="Name">
<TextInput <TextInput
style={styles.input} style={styles.input}
@@ -76,9 +82,14 @@ export default function ItemModal({
</Field> </Field>
) : null} ) : null}
<Pressable style={styles.secondaryBtn} onPress={pickItemImage}> <View style={styles.actionRow}>
<Text style={styles.secondaryBtnText}>{itemForm.imageUri ? 'Change image' : 'Add image'}</Text> <Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={takeItemImage}>
<Text style={styles.secondaryBtnText}>Take photo</Text>
</Pressable> </Pressable>
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={pickItemImage}>
<Text style={styles.secondaryBtnText}>{itemForm.imageUri ? 'From gallery (change)' : 'From gallery'}</Text>
</Pressable>
</View>
{!!itemForm.imageUri && <Image source={{ uri: itemForm.imageUri }} style={styles.previewImageSmall} />} {!!itemForm.imageUri && <Image source={{ uri: itemForm.imageUri }} style={styles.previewImageSmall} />}
<Pressable style={styles.primaryBtn} onPress={saveItemFromModal}> <Pressable style={styles.primaryBtn} onPress={saveItemFromModal}>

View File

@@ -211,6 +211,11 @@ export const styles = StyleSheet.create({
color: '#dbeafe', color: '#dbeafe',
fontWeight: '700', fontWeight: '700',
}, },
actionRow: {
flexDirection: 'row',
gap: 8,
marginTop: 4,
},
inlineToggle: { inlineToggle: {
marginTop: 2, marginTop: 2,
@@ -400,20 +405,23 @@ export const styles = StyleSheet.create({
modalBackdrop: { modalBackdrop: {
flex: 1, flex: 1,
backgroundColor: 'rgba(2,6,23,0.72)', backgroundColor: 'rgba(2,6,23,0.72)',
justifyContent: 'flex-end', paddingHorizontal: 12,
}, },
modalKeyboardWrap: { modalKeyboardWrap: {
flex: 1,
width: '100%', width: '100%',
alignItems: 'center',
justifyContent: 'center',
}, },
modalCard: { modalCard: {
maxHeight: '87%', width: '96%',
maxHeight: '90%',
backgroundColor: '#0f172a', backgroundColor: '#0f172a',
borderTopLeftRadius: 18, borderRadius: 20,
borderTopRightRadius: 18,
borderWidth: 1, borderWidth: 1,
borderColor: '#1e293b', borderColor: '#1e293b',
padding: 14, padding: 16,
gap: 8, gap: 10,
}, },
closeText: { closeText: {
color: '#93c5fd', color: '#93c5fd',

View File

@@ -17,6 +17,7 @@ export default function TripsTab({
tripForm, tripForm,
updateTripForm, updateTripForm,
pickTripImage, pickTripImage,
takeTripImage,
templateTrip, templateTrip,
createTrip, createTrip,
trips, trips,
@@ -58,9 +59,14 @@ export default function TripsTab({
<DateField label="Start Date" value={tripForm.startDate} onPress={() => openDatePicker('startDate')} /> <DateField label="Start Date" value={tripForm.startDate} onPress={() => openDatePicker('startDate')} />
<DateField label="End Date" value={tripForm.endDate} onPress={() => openDatePicker('endDate')} /> <DateField label="End Date" value={tripForm.endDate} onPress={() => openDatePicker('endDate')} />
<Pressable style={styles.secondaryBtn} onPress={pickTripImage}> <View style={styles.actionRow}>
<Text style={styles.secondaryBtnText}>{tripForm.imageUri ? 'Change trip image' : 'Add trip image'}</Text> <Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={takeTripImage}>
<Text style={styles.secondaryBtnText}>Take photo</Text>
</Pressable> </Pressable>
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={pickTripImage}>
<Text style={styles.secondaryBtnText}>{tripForm.imageUri ? 'From gallery (change)' : 'From gallery'}</Text>
</Pressable>
</View>
{tripForm.imageUri ? <Image source={{ uri: tripForm.imageUri }} style={styles.previewImage} /> : null} {tripForm.imageUri ? <Image source={{ uri: tripForm.imageUri }} style={styles.previewImage} /> : null}