Compare commits
3 Commits
luggage-li
...
luggage-li
| Author | SHA1 | Date | |
|---|---|---|---|
| fbdae66c9b | |||
| e8ffab355e | |||
| 2ec877362f |
@@ -22,7 +22,6 @@ const emptyTripForm = () => ({
|
|||||||
location: '',
|
location: '',
|
||||||
startDate: todayYMD(),
|
startDate: todayYMD(),
|
||||||
endDate: todayYMD(),
|
endDate: todayYMD(),
|
||||||
imageUri: '',
|
|
||||||
copyDefaultTemplate: true,
|
copyDefaultTemplate: true,
|
||||||
setAsDefaultTemplate: false,
|
setAsDefaultTemplate: false,
|
||||||
});
|
});
|
||||||
@@ -279,7 +278,7 @@ export default function AppRoot() {
|
|||||||
location: tripForm.location.trim(),
|
location: tripForm.location.trim(),
|
||||||
startDate: tripForm.startDate,
|
startDate: tripForm.startDate,
|
||||||
endDate: tripForm.endDate,
|
endDate: tripForm.endDate,
|
||||||
imageUri: tripForm.imageUri,
|
imageUri: '',
|
||||||
archived: false,
|
archived: false,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -344,7 +343,6 @@ export default function AppRoot() {
|
|||||||
location: patch.location.trim(),
|
location: patch.location.trim(),
|
||||||
startDate: patch.startDate,
|
startDate: patch.startDate,
|
||||||
endDate: patch.endDate,
|
endDate: patch.endDate,
|
||||||
imageUri: patch.imageUri,
|
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}
|
}
|
||||||
: trip
|
: trip
|
||||||
@@ -727,6 +725,14 @@ export default function AppRoot() {
|
|||||||
}, 80);
|
}, 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openQuickAddItemFromNav() {
|
||||||
|
if (!selectedTripId) {
|
||||||
|
showAlert('No trip selected', 'Please select or create a trip first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openAddItemModal();
|
||||||
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.safe, { paddingTop: topInset }]}>
|
<SafeAreaView style={[styles.safe, { paddingTop: topInset }]}>
|
||||||
@@ -755,18 +761,6 @@ export default function AppRoot() {
|
|||||||
<TripsTab
|
<TripsTab
|
||||||
tripForm={tripForm}
|
tripForm={tripForm}
|
||||||
updateTripForm={updateTripForm}
|
updateTripForm={updateTripForm}
|
||||||
pickTripImage={(onPicked) =>
|
|
||||||
pickImage((uri) => {
|
|
||||||
if (typeof onPicked === 'function') onPicked(uri);
|
|
||||||
else updateTripForm('imageUri', uri);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
takeTripImage={(onPicked) =>
|
|
||||||
takeImage((uri) => {
|
|
||||||
if (typeof onPicked === 'function') onPicked(uri);
|
|
||||||
else updateTripForm('imageUri', uri);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
templateTrip={templateTrip}
|
templateTrip={templateTrip}
|
||||||
createTrip={createTrip}
|
createTrip={createTrip}
|
||||||
trips={data.trips}
|
trips={data.trips}
|
||||||
@@ -817,7 +811,7 @@ export default function AppRoot() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
<BottomTab current={tab} onChange={setTab} />
|
<BottomTab current={tab} onChange={setTab} onAddItem={openQuickAddItemFromNav} canAddItem={!!selectedTripId} />
|
||||||
|
|
||||||
<DatePickerModal
|
<DatePickerModal
|
||||||
visible={datePicker.visible}
|
visible={datePicker.visible}
|
||||||
|
|||||||
@@ -3,10 +3,23 @@ import { Pressable, Text, View } from 'react-native';
|
|||||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||||
import { styles } from '../styles';
|
import { styles } from '../styles';
|
||||||
|
|
||||||
export default function BottomTab({ current, onChange }) {
|
function TabBtn({ tab, current, onChange }) {
|
||||||
const tabs = [
|
const active = current === tab.key;
|
||||||
|
return (
|
||||||
|
<Pressable onPress={() => onChange(tab.key)} style={styles.tabItem}>
|
||||||
|
<Ionicons name={active ? tab.iconActive : tab.icon} size={18} color={active ? '#dbeafe' : '#94a3b8'} />
|
||||||
|
<Text style={[styles.tabLabel, active && styles.tabLabelActive]}>{tab.label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BottomTab({ current, onChange, onAddItem, canAddItem }) {
|
||||||
|
const leftTabs = [
|
||||||
{ key: 'trips', label: 'Trips', icon: 'airplane-outline', iconActive: 'airplane' },
|
{ key: 'trips', label: 'Trips', icon: 'airplane-outline', iconActive: 'airplane' },
|
||||||
{ key: 'items', label: 'Items', icon: 'briefcase-outline', iconActive: 'briefcase' },
|
{ key: 'items', label: 'Items', icon: 'briefcase-outline', iconActive: 'briefcase' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rightTabs = [
|
||||||
{ key: 'checkup', label: 'Check-Up', icon: 'checkmark-circle-outline', iconActive: 'checkmark-circle' },
|
{ key: 'checkup', label: 'Check-Up', icon: 'checkmark-circle-outline', iconActive: 'checkmark-circle' },
|
||||||
{ key: 'history', label: 'History', icon: 'time-outline', iconActive: 'time' },
|
{ key: 'history', label: 'History', icon: 'time-outline', iconActive: 'time' },
|
||||||
];
|
];
|
||||||
@@ -14,19 +27,25 @@ export default function BottomTab({ current, onChange }) {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.tabBarWrap}>
|
<View style={styles.tabBarWrap}>
|
||||||
<View style={styles.tabBar}>
|
<View style={styles.tabBar}>
|
||||||
{tabs.map((tab) => {
|
<View style={styles.tabGroupSide}>
|
||||||
const active = current === tab.key;
|
{leftTabs.map((tab) => (
|
||||||
return (
|
<TabBtn key={tab.key} tab={tab} current={current} onChange={onChange} />
|
||||||
<Pressable key={tab.key} onPress={() => onChange(tab.key)} style={styles.tabItem}>
|
))}
|
||||||
<Ionicons
|
</View>
|
||||||
name={active ? tab.iconActive : tab.icon}
|
|
||||||
size={18}
|
<Pressable
|
||||||
color={active ? '#dbeafe' : '#94a3b8'}
|
style={[styles.tabAddBtn, !canAddItem && styles.tabAddBtnDisabled]}
|
||||||
/>
|
onPress={onAddItem}
|
||||||
<Text style={[styles.tabLabel, active && styles.tabLabelActive]}>{tab.label}</Text>
|
disabled={!canAddItem}
|
||||||
</Pressable>
|
>
|
||||||
);
|
<Ionicons name="add" size={26} color="#fff" />
|
||||||
})}
|
</Pressable>
|
||||||
|
|
||||||
|
<View style={styles.tabGroupSide}>
|
||||||
|
{rightTabs.map((tab) => (
|
||||||
|
<TabBtn key={tab.key} tab={tab} current={current} onChange={onChange} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
|||||||
return (
|
return (
|
||||||
<View style={styles.itemCard}>
|
<View style={styles.itemCard}>
|
||||||
<View style={[styles.itemAccent, { backgroundColor: statusAccent(item.status) }]} />
|
<View style={[styles.itemAccent, { backgroundColor: statusAccent(item.status) }]} />
|
||||||
|
|
||||||
|
<View style={styles.itemThumbWrap}>
|
||||||
|
{item.imageUri ? (
|
||||||
|
<Image source={{ uri: item.imageUri }} style={styles.itemThumbSmall} />
|
||||||
|
) : (
|
||||||
|
<View style={styles.itemThumbPlaceholder}>
|
||||||
|
<Text style={styles.itemThumbPlaceholderText}>🧳</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.itemMain}>
|
<View style={styles.itemMain}>
|
||||||
<View style={styles.cardRow}>
|
<View style={styles.cardRow}>
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
@@ -41,8 +52,6 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
|||||||
<Text style={[styles.quickStatusBtnText, isUnpacked && styles.quickStatusBtnTextActive]}>Unpack</Text>
|
<Text style={[styles.quickStatusBtnText, isUnpacked && styles.quickStatusBtnTextActive]}>Unpack</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{item.imageUri ? <Image source={{ uri: item.imageUri }} style={styles.previewImageSmall} /> : null}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -282,13 +282,38 @@ export const styles = StyleSheet.create({
|
|||||||
borderColor: '#1f2937',
|
borderColor: '#1f2937',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
},
|
},
|
||||||
itemAccent: {
|
itemAccent: {
|
||||||
width: 5,
|
width: 5,
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
itemThumbWrap: {
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingLeft: 10,
|
||||||
|
},
|
||||||
|
itemThumbSmall: {
|
||||||
|
width: 46,
|
||||||
|
height: 46,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#0b1220',
|
||||||
|
},
|
||||||
|
itemThumbPlaceholder: {
|
||||||
|
width: 46,
|
||||||
|
height: 46,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#0b1220',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#243244',
|
||||||
|
},
|
||||||
|
itemThumbPlaceholderText: {
|
||||||
|
fontSize: 18,
|
||||||
},
|
},
|
||||||
itemMain: {
|
itemMain: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 12,
|
padding: 10,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
itemTitle: {
|
itemTitle: {
|
||||||
@@ -462,13 +487,34 @@ export const styles = StyleSheet.create({
|
|||||||
borderColor: '#1f2937',
|
borderColor: '#1f2937',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
tabGroupSide: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
width: '42%',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
minWidth: 62,
|
minWidth: 58,
|
||||||
|
},
|
||||||
|
tabAddBtn: {
|
||||||
|
width: 54,
|
||||||
|
height: 54,
|
||||||
|
borderRadius: 999,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
marginTop: -24,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#0b1220',
|
||||||
|
},
|
||||||
|
tabAddBtnDisabled: {
|
||||||
|
opacity: 0.45,
|
||||||
},
|
},
|
||||||
tabLabel: {
|
tabLabel: {
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
import { KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
|
||||||
import DatePickerModal from '../components/DatePickerModal';
|
import DatePickerModal from '../components/DatePickerModal';
|
||||||
import Field from '../components/Field';
|
import Field from '../components/Field';
|
||||||
import { styles } from '../styles';
|
import { styles } from '../styles';
|
||||||
@@ -19,14 +19,11 @@ const emptyEditForm = {
|
|||||||
location: '',
|
location: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
imageUri: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TripsTab({
|
export default function TripsTab({
|
||||||
tripForm,
|
tripForm,
|
||||||
updateTripForm,
|
updateTripForm,
|
||||||
pickTripImage,
|
|
||||||
takeTripImage,
|
|
||||||
templateTrip,
|
templateTrip,
|
||||||
createTrip,
|
createTrip,
|
||||||
trips,
|
trips,
|
||||||
@@ -68,7 +65,6 @@ export default function TripsTab({
|
|||||||
location: trip.location || '',
|
location: trip.location || '',
|
||||||
startDate: trip.startDate || '',
|
startDate: trip.startDate || '',
|
||||||
endDate: trip.endDate || '',
|
endDate: trip.endDate || '',
|
||||||
imageUri: trip.imageUri || '',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,14 +79,6 @@ export default function TripsTab({
|
|||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickViewTripImage() {
|
|
||||||
pickTripImage((uri) => updateEditForm('imageUri', uri));
|
|
||||||
}
|
|
||||||
|
|
||||||
function takeViewTripImage() {
|
|
||||||
takeTripImage((uri) => updateEditForm('imageUri', uri));
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTemplateFromView() {
|
function applyTemplateFromView() {
|
||||||
if (!viewingTrip) return;
|
if (!viewingTrip) return;
|
||||||
setTripAsTemplate(viewingTrip.id);
|
setTripAsTemplate(viewingTrip.id);
|
||||||
@@ -124,7 +112,6 @@ export default function TripsTab({
|
|||||||
|
|
||||||
{activeTrip ? (
|
{activeTrip ? (
|
||||||
<View style={styles.tripHeroCard}>
|
<View style={styles.tripHeroCard}>
|
||||||
{activeTrip.imageUri ? <Image source={{ uri: activeTrip.imageUri }} style={styles.tripHeroImage} /> : null}
|
|
||||||
<Text style={styles.tripHeroTitle}>{activeTrip.name}</Text>
|
<Text style={styles.tripHeroTitle}>{activeTrip.name}</Text>
|
||||||
<Text style={styles.cardMeta}>{activeTrip.location || 'No location'} · {activeTrip.startDate} → {activeTrip.endDate}</Text>
|
<Text style={styles.cardMeta}>{activeTrip.location || 'No location'} · {activeTrip.startDate} → {activeTrip.endDate}</Text>
|
||||||
<Text style={styles.cardMeta}>{activeTripItemCount} items · {activeTripCheckupCount} check-ups</Text>
|
<Text style={styles.cardMeta}>{activeTripItemCount} items · {activeTripCheckupCount} check-ups</Text>
|
||||||
@@ -155,7 +142,6 @@ export default function TripsTab({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{trip.imageUri ? <Image source={{ uri: trip.imageUri }} style={styles.previewImageSmall} /> : null}
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -226,17 +212,6 @@ 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')} />
|
||||||
|
|
||||||
<View style={styles.actionRow}>
|
|
||||||
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={takeTripImage}>
|
|
||||||
<Text style={styles.secondaryBtnText}>Take photo</Text>
|
|
||||||
</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}
|
|
||||||
|
|
||||||
{templateTrip ? (
|
{templateTrip ? (
|
||||||
<Pressable style={styles.inlineToggle} onPress={() => updateTripForm('copyDefaultTemplate', !tripForm.copyDefaultTemplate)}>
|
<Pressable style={styles.inlineToggle} onPress={() => updateTripForm('copyDefaultTemplate', !tripForm.copyDefaultTemplate)}>
|
||||||
<Text style={styles.inlineToggleText}>
|
<Text style={styles.inlineToggleText}>
|
||||||
@@ -283,7 +258,6 @@ export default function TripsTab({
|
|||||||
>
|
>
|
||||||
{!editMode ? (
|
{!editMode ? (
|
||||||
<>
|
<>
|
||||||
{viewingTrip.imageUri ? <Image source={{ uri: viewingTrip.imageUri }} style={styles.previewImage} /> : null}
|
|
||||||
<Text style={styles.tripHeroTitle}>{viewingTrip.name}</Text>
|
<Text style={styles.tripHeroTitle}>{viewingTrip.name}</Text>
|
||||||
<Text style={styles.cardMeta}>{viewingTrip.location || 'No location'}</Text>
|
<Text style={styles.cardMeta}>{viewingTrip.location || 'No location'}</Text>
|
||||||
<Text style={styles.cardMeta}>{viewingTrip.startDate} → {viewingTrip.endDate}</Text>
|
<Text style={styles.cardMeta}>{viewingTrip.startDate} → {viewingTrip.endDate}</Text>
|
||||||
@@ -333,17 +307,6 @@ export default function TripsTab({
|
|||||||
<DateField label="Start Date" value={editForm.startDate} onPress={() => setViewDatePicker({ visible: true, field: 'startDate' })} />
|
<DateField label="Start Date" value={editForm.startDate} onPress={() => setViewDatePicker({ visible: true, field: 'startDate' })} />
|
||||||
<DateField label="End Date" value={editForm.endDate} onPress={() => setViewDatePicker({ visible: true, field: 'endDate' })} />
|
<DateField label="End Date" value={editForm.endDate} onPress={() => setViewDatePicker({ visible: true, field: 'endDate' })} />
|
||||||
|
|
||||||
<View style={styles.actionRow}>
|
|
||||||
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={takeViewTripImage}>
|
|
||||||
<Text style={styles.secondaryBtnText}>Take photo</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={pickViewTripImage}>
|
|
||||||
<Text style={styles.secondaryBtnText}>{editForm.imageUri ? 'From gallery (change)' : 'From gallery'}</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{editForm.imageUri ? <Image source={{ uri: editForm.imageUri }} style={styles.previewImage} /> : null}
|
|
||||||
|
|
||||||
<Pressable style={styles.primaryBtn} onPress={saveEditFromView}>
|
<Pressable style={styles.primaryBtn} onPress={saveEditFromView}>
|
||||||
<Text style={styles.primaryBtnText}>Save Trip</Text>
|
<Text style={styles.primaryBtnText}>Save Trip</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
Reference in New Issue
Block a user