diff --git a/TODO.md b/TODO.md
index 4dd7986..b12e180 100644
--- a/TODO.md
+++ b/TODO.md
@@ -39,11 +39,14 @@ Improving & Fixing Bugs (V3)
- [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)
+- [x] Added trip view editing in modal (name/location/date/image)
+- [x] Added trip archive/unarchive flow with archived section
+- [x] Added item filters + bulk pack/unpack actions
## Next Improvements (Requested)
-- [ ] Edit trip directly in the trip view modal (name/location/dates/image)
-- [ ] Add archive flow for trips (hide from active list without deleting history)
+- [x] Edit trip directly in the trip view modal (name/location/dates/image)
+- [x] Add archive flow for trips (hide from active list without deleting history)
- [ ] Enhance check-up modal UX (progress bar + back/skip controls)
-- [ ] Add bulk item actions and filters (pack all/unpack all + status/category chips)
+- [x] Add bulk item actions and filters (pack all/unpack all + status/category chips)
- [ ] Add image optimization controls before save (compress/crop)
- [ ] Add JSON export/import backup + restore flow
diff --git a/src/AppRoot.js b/src/AppRoot.js
index e1c4cd7..1d92a67 100644
--- a/src/AppRoot.js
+++ b/src/AppRoot.js
@@ -83,7 +83,9 @@ export default function AppRoot() {
const topInset = Platform.OS === 'android' ? (RNStatusBar.currentHeight || 0) + 10 : 0;
- const selectedTrip = useMemo(() => data.trips.find((trip) => trip.id === selectedTripId) || null, [data.trips, selectedTripId]);
+ const visibleTrips = useMemo(() => data.trips.filter((trip) => !trip.archived), [data.trips]);
+
+ const selectedTrip = useMemo(() => visibleTrips.find((trip) => trip.id === selectedTripId) || null, [visibleTrips, selectedTripId]);
const selectedTripItems = useMemo(() => {
if (!selectedTripId) return [];
@@ -139,18 +141,18 @@ export default function AppRoot() {
useEffect(() => {
if (!loaded) return;
- if (!data.trips.length) {
+ if (!visibleTrips.length) {
setSelectedTripId(null);
return;
}
- if (selectedTripId && data.trips.some((trip) => trip.id === selectedTripId)) {
+ if (selectedTripId && visibleTrips.some((trip) => trip.id === selectedTripId)) {
return;
}
- const bestTripId = findBestTripId(data.trips);
- setSelectedTripId(bestTripId || data.trips[0].id);
- }, [data.trips, selectedTripId, loaded]);
+ const bestTripId = findBestTripId(visibleTrips);
+ setSelectedTripId(bestTripId || visibleTrips[0].id);
+ }, [visibleTrips, selectedTripId, loaded]);
useEffect(() => {
if (tab !== 'checkup') return;
@@ -244,6 +246,7 @@ export default function AppRoot() {
startDate: tripForm.startDate,
endDate: tripForm.endDate,
imageUri: tripForm.imageUri,
+ archived: false,
createdAt: now,
updatedAt: now,
},
@@ -278,6 +281,62 @@ export default function AppRoot() {
setData((prev) => ({ ...prev, defaultTemplateTripId: tripId }));
}
+ function saveTripEdits(tripId, patch) {
+ if (!patch.name.trim()) {
+ Alert.alert('Missing name', 'Trip name is required.');
+ return false;
+ }
+
+ const start = parseYMD(patch.startDate);
+ const end = parseYMD(patch.endDate);
+
+ if (!start || !end) {
+ Alert.alert('Invalid dates', 'Please select valid trip dates.');
+ return false;
+ }
+
+ if (start > end) {
+ Alert.alert('Invalid dates', 'Start date cannot be after end date.');
+ return false;
+ }
+
+ setData((prev) => ({
+ ...prev,
+ trips: prev.trips.map((trip) =>
+ trip.id === tripId
+ ? {
+ ...trip,
+ name: patch.name.trim(),
+ location: patch.location.trim(),
+ startDate: patch.startDate,
+ endDate: patch.endDate,
+ imageUri: patch.imageUri,
+ updatedAt: Date.now(),
+ }
+ : trip
+ ),
+ }));
+
+ return true;
+ }
+
+ function setTripArchived(tripId, archived) {
+ setData((prev) => ({
+ ...prev,
+ trips: prev.trips.map((trip) =>
+ trip.id === tripId
+ ? {
+ ...trip,
+ archived,
+ archivedAt: archived ? Date.now() : null,
+ updatedAt: Date.now(),
+ }
+ : trip
+ ),
+ defaultTemplateTripId: archived && prev.defaultTemplateTripId === tripId ? null : prev.defaultTemplateTripId,
+ }));
+ }
+
function deleteTrip(tripId) {
Alert.alert('Delete trip?', 'Trip items and check-up history will also be deleted.', [
{ text: 'Cancel', style: 'cancel' },
@@ -406,6 +465,30 @@ export default function AppRoot() {
});
}
+ function bulkSetItemStatus(itemIds, status) {
+ if (!selectedTripId || !itemIds.length) return;
+ const idSet = new Set(itemIds);
+ setData((prev) => {
+ const items = prev.itemsByTrip[selectedTripId] || [];
+ return {
+ ...prev,
+ itemsByTrip: {
+ ...prev.itemsByTrip,
+ [selectedTripId]: items.map((item) =>
+ idSet.has(item.id)
+ ? {
+ ...item,
+ status,
+ lentTo: status === 'lent-to' ? item.lentTo : '',
+ updatedAt: Date.now(),
+ }
+ : item
+ ),
+ },
+ };
+ });
+ }
+
function deleteCheckup(checkupId) {
if (!selectedTripId) return;
setData((prev) => {
@@ -624,20 +707,32 @@ export default function AppRoot() {
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
-
+
{tab === 'trips' && (
pickImage((uri) => updateTripForm('imageUri', uri))}
- takeTripImage={() => takeImage((uri) => updateTripForm('imageUri', uri))}
+ 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}
createTrip={createTrip}
trips={data.trips}
selectedTripId={selectedTripId}
chooseTrip={setSelectedTripId}
setTripAsTemplate={setTripAsTemplate}
+ saveTripEdits={saveTripEdits}
+ setTripArchived={setTripArchived}
deleteTrip={deleteTrip}
onInputFocus={onInputFocus}
defaultTemplateTripId={data.defaultTemplateTripId}
@@ -655,6 +750,7 @@ export default function AppRoot() {
openEditItemModal={openEditItemModal}
deleteItem={deleteItem}
quickSetItemStatus={quickSetItemStatus}
+ bulkSetItemStatus={bulkSetItemStatus}
/>
)}
diff --git a/src/styles.js b/src/styles.js
index 32ccb9d..fd89d7d 100644
--- a/src/styles.js
+++ b/src/styles.js
@@ -89,6 +89,10 @@ export const styles = StyleSheet.create({
cardActive: {
borderColor: '#60a5fa',
},
+ cardArchived: {
+ opacity: 0.72,
+ borderColor: '#374151',
+ },
cardSoft: {
backgroundColor: '#0f172a',
borderRadius: 16,
diff --git a/src/tabs/ItemsTab.js b/src/tabs/ItemsTab.js
index 7c2abc4..b197a4d 100644
--- a/src/tabs/ItemsTab.js
+++ b/src/tabs/ItemsTab.js
@@ -1,6 +1,7 @@
-import React from 'react';
+import React, { useMemo, useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import ItemCard from '../components/ItemCard';
+import { ITEM_STATUSES } from '../constants';
import { styles } from '../styles';
export default function ItemsTab({
@@ -10,7 +11,30 @@ export default function ItemsTab({
openEditItemModal,
deleteItem,
quickSetItemStatus,
+ bulkSetItemStatus,
}) {
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [categoryFilter, setCategoryFilter] = useState('all');
+
+ const categories = useMemo(
+ () => Array.from(new Set(selectedTripItems.map((item) => item.category?.trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b)),
+ [selectedTripItems]
+ );
+
+ const filteredItems = useMemo(
+ () =>
+ selectedTripItems.filter((item) => {
+ const matchStatus = statusFilter === 'all' || item.status === statusFilter;
+ const itemCategory = item.category?.trim() || '';
+ const matchCategory = categoryFilter === 'all' || itemCategory === categoryFilter;
+ return matchStatus && matchCategory;
+ }),
+ [selectedTripItems, statusFilter, categoryFilter]
+ );
+
+ const filterStatusOptions = ['all', ...ITEM_STATUSES];
+ const filterCategoryOptions = ['all', ...categories];
+
return (
@@ -23,7 +47,51 @@ export default function ItemsTab({
{!selectedTrip ? Select a trip first. : null}
{selectedTripItems.length === 0 && selectedTrip ? No items yet. : null}
- {selectedTripItems.map((item) => (
+ {selectedTrip ? (
+
+ Quick actions
+
+ bulkSetItemStatus(filteredItems.map((x) => x.id), 'packed')}>
+ Pack shown ({filteredItems.length})
+
+ bulkSetItemStatus(filteredItems.map((x) => x.id), 'unpacked')}>
+ Unpack shown ({filteredItems.length})
+
+
+
+ ) : null}
+
+ {selectedTripItems.length > 0 ? (
+
+ Filters
+
+ Status
+
+ {filterStatusOptions.map((status) => {
+ const active = statusFilter === status;
+ return (
+ setStatusFilter(status)}>
+ {status}
+
+ );
+ })}
+
+
+ Category
+
+ {filterCategoryOptions.map((category) => {
+ const active = categoryFilter === category;
+ return (
+ setCategoryFilter(category)}>
+ {category}
+
+ );
+ })}
+
+
+ ) : null}
+
+ {filteredItems.map((item) => (
quickSetItemStatus(item.id, 'unpacked')}
/>
))}
+
+ {selectedTripItems.length > 0 && filteredItems.length === 0 ? No items match the current filters. : null}
);
}
diff --git a/src/tabs/TripsTab.js b/src/tabs/TripsTab.js
index 5ececf8..a276762 100644
--- a/src/tabs/TripsTab.js
+++ b/src/tabs/TripsTab.js
@@ -1,5 +1,6 @@
import React, { useMemo, useState } from 'react';
import { Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
+import DatePickerModal from '../components/DatePickerModal';
import Field from '../components/Field';
import { styles } from '../styles';
@@ -13,6 +14,14 @@ function DateField({ label, value, onPress }) {
);
}
+const emptyEditForm = {
+ name: '',
+ location: '',
+ startDate: '',
+ endDate: '',
+ imageUri: '',
+};
+
export default function TripsTab({
tripForm,
updateTripForm,
@@ -24,6 +33,8 @@ export default function TripsTab({
selectedTripId,
chooseTrip,
setTripAsTemplate,
+ saveTripEdits,
+ setTripArchived,
deleteTrip,
onInputFocus,
defaultTemplateTripId,
@@ -33,24 +44,72 @@ export default function TripsTab({
}) {
const [createModalVisible, setCreateModalVisible] = useState(false);
const [viewTripId, setViewTripId] = useState(null);
+ const [editMode, setEditMode] = useState(false);
+ const [editForm, setEditForm] = useState(emptyEditForm);
+ const [viewDatePicker, setViewDatePicker] = useState({ visible: false, field: 'startDate' });
const activeTrip = useMemo(() => trips.find((trip) => trip.id === selectedTripId) || null, [trips, selectedTripId]);
const viewingTrip = useMemo(() => trips.find((trip) => trip.id === viewTripId) || null, [trips, viewTripId]);
+ const activeTrips = useMemo(() => trips.filter((trip) => !trip.archived), [trips]);
+ const archivedTrips = useMemo(() => trips.filter((trip) => trip.archived), [trips]);
function submitCreateTrip() {
const ok = createTrip();
if (ok) setCreateModalVisible(false);
}
+ function openView(tripId) {
+ const trip = trips.find((x) => x.id === tripId);
+ if (!trip) return;
+ setViewTripId(tripId);
+ setEditMode(false);
+ setEditForm({
+ name: trip.name || '',
+ location: trip.location || '',
+ startDate: trip.startDate || '',
+ endDate: trip.endDate || '',
+ imageUri: trip.imageUri || '',
+ });
+ }
+
+ function updateEditForm(field, value) {
+ setEditForm((prev) => ({ ...prev, [field]: value }));
+ }
+
+ function saveEditFromView() {
+ if (!viewingTrip) return;
+ const ok = saveTripEdits(viewingTrip.id, editForm);
+ if (!ok) return;
+ setEditMode(false);
+ }
+
+ function pickViewTripImage() {
+ pickTripImage((uri) => updateEditForm('imageUri', uri));
+ }
+
+ function takeViewTripImage() {
+ takeTripImage((uri) => updateEditForm('imageUri', uri));
+ }
+
function applyTemplateFromView() {
if (!viewingTrip) return;
setTripAsTemplate(viewingTrip.id);
}
+ function toggleArchiveFromView() {
+ if (!viewingTrip) return;
+ setTripArchived(viewingTrip.id, !viewingTrip.archived);
+ if (!viewingTrip.archived) {
+ setViewTripId(null);
+ setEditMode(false);
+ }
+ }
+
function deleteFromView() {
if (!viewingTrip) return;
const tripId = viewingTrip.id;
setViewTripId(null);
+ setEditMode(false);
deleteTrip(tripId);
}
@@ -71,12 +130,12 @@ export default function TripsTab({
{activeTripItemCount} items · {activeTripCheckupCount} check-ups
) : (
- Create your first trip to get started.
+ No active trips. Unarchive or create one.
)}
- All Trips
+ Active Trips
- {trips
+ {activeTrips
.slice()
.sort((a, b) => b.startDate.localeCompare(a.startDate))
.map((trip) => (
@@ -91,7 +150,7 @@ export default function TripsTab({
chooseTrip(trip.id)}>
Select
- setViewTripId(trip.id)}>
+ openView(trip.id)}>
View
@@ -100,6 +159,31 @@ export default function TripsTab({
))}
+ {archivedTrips.length > 0 ? (
+ <>
+ Archived Trips
+ {archivedTrips
+ .slice()
+ .sort((a, b) => b.startDate.localeCompare(a.startDate))
+ .map((trip) => (
+
+
+
+ {trip.name}
+ {trip.location || 'No location'} · {trip.startDate} → {trip.endDate}
+ Archived
+
+
+ openView(trip.id)}>
+ View
+
+
+
+
+ ))}
+ >
+ ) : null}
+
@@ -180,7 +264,12 @@ export default function TripsTab({
Trip View
- setViewTripId(null)}>
+ {
+ setViewTripId(null);
+ setEditMode(false);
+ }}
+ >
Close
@@ -192,25 +281,94 @@ export default function TripsTab({
contentContainerStyle={{ paddingBottom: 12 }}
showsVerticalScrollIndicator={false}
>
- {viewingTrip.imageUri ? : null}
- {viewingTrip.name}
- {viewingTrip.location || 'No location'}
- {viewingTrip.startDate} → {viewingTrip.endDate}
- {defaultTemplateTripId === viewingTrip.id ? 'Default template trip' : 'Not default template'}
+ {!editMode ? (
+ <>
+ {viewingTrip.imageUri ? : null}
+ {viewingTrip.name}
+ {viewingTrip.location || 'No location'}
+ {viewingTrip.startDate} → {viewingTrip.endDate}
+ {defaultTemplateTripId === viewingTrip.id ? 'Default template trip' : 'Not default template'}
+ {viewingTrip.archived ? 'Archived' : 'Active'}
-
- Set as Template
-
+ setEditMode(true)}>
+ Edit Trip
+
-
- Delete Trip
-
+
+ Set as Template
+
+
+
+ {viewingTrip.archived ? 'Unarchive Trip' : 'Archive Trip'}
+
+
+
+ Delete Trip
+
+ >
+ ) : (
+ <>
+
+ updateEditForm('name', v)}
+ placeholder="Trip name"
+ placeholderTextColor="#6b7280"
+ onFocus={onInputFocus}
+ />
+
+
+
+ updateEditForm('location', v)}
+ placeholder="Location"
+ placeholderTextColor="#6b7280"
+ onFocus={onInputFocus}
+ />
+
+
+ setViewDatePicker({ visible: true, field: 'startDate' })} />
+ setViewDatePicker({ visible: true, field: 'endDate' })} />
+
+
+
+ Take photo
+
+
+ {editForm.imageUri ? 'From gallery (change)' : 'From gallery'}
+
+
+
+ {editForm.imageUri ? : null}
+
+
+ Save Trip
+
+ setEditMode(false)}>
+ Cancel Edit
+
+ >
+ )}
) : null}
+
+ setViewDatePicker((prev) => ({ ...prev, visible: false }))}
+ onSelect={(ymd) => {
+ updateEditForm(viewDatePicker.field, ymd);
+ setViewDatePicker((prev) => ({ ...prev, visible: false }));
+ }}
+ />
);
}