diff --git a/src/AppRoot.js b/src/AppRoot.js
index e32a209..6097049 100644
--- a/src/AppRoot.js
+++ b/src/AppRoot.js
@@ -7,7 +7,7 @@ import BottomTab from './components/BottomTab';
import TripPicker from './components/TripPicker';
import DatePickerModal from './components/DatePickerModal';
import ItemModal from './modals/ItemModal';
-import CheckupFixModal from './modals/CheckupFixModal';
+import CheckupFlowModal from './modals/CheckupFlowModal';
import TripsTab from './tabs/TripsTab';
import ItemsTab from './tabs/ItemsTab';
import CheckupTab from './tabs/CheckupTab';
@@ -37,6 +37,28 @@ const emptyItemForm = () => ({
imageUri: '',
});
+const emptyCheckupNoForm = () => ({
+ status: 'unpacked',
+ placement: 'suitcase',
+ lentTo: '',
+ updateMasterList: false,
+});
+
+function buildCheckupSession(items) {
+ return items.map((item) => ({
+ itemId: item.id,
+ name: item.name,
+ category: item.category,
+ current: {
+ status: item.status,
+ placement: item.placement,
+ lentTo: item.lentTo || '',
+ },
+ confirmed: false,
+ result: 'pending',
+ }));
+}
+
export default function AppRoot() {
const scrollRef = useRef(null);
@@ -52,14 +74,10 @@ export default function AppRoot() {
const [itemForm, setItemForm] = useState(emptyItemForm());
const [checkupSession, setCheckupSession] = useState([]);
- const [checkupFixModalVisible, setCheckupFixModalVisible] = useState(false);
- const [checkupFixTargetId, setCheckupFixTargetId] = useState(null);
- const [checkupFixForm, setCheckupFixForm] = useState({
- status: 'unpacked',
- placement: 'suitcase',
- lentTo: '',
- updateMasterList: false,
- });
+ const [checkupFlowVisible, setCheckupFlowVisible] = useState(false);
+ const [checkupFlowIndex, setCheckupFlowIndex] = useState(0);
+ const [checkupFlowMode, setCheckupFlowMode] = useState('question');
+ const [checkupNoForm, setCheckupNoForm] = useState(emptyCheckupNoForm());
const [selectedCheckupId, setSelectedCheckupId] = useState(null);
@@ -90,6 +108,12 @@ export default function AppRoot() {
return { total, correct, bad, pending };
}, [checkupSession]);
+ const checkupCurrentEntry = useMemo(() => {
+ if (!checkupFlowVisible) return null;
+ if (checkupFlowIndex >= checkupSession.length) return null;
+ return checkupSession[checkupFlowIndex] || null;
+ }, [checkupFlowVisible, checkupFlowIndex, checkupSession]);
+
useEffect(() => {
(async () => {
try {
@@ -189,7 +213,7 @@ export default function AppRoot() {
function createTrip() {
if (!tripForm.name.trim()) {
Alert.alert('Missing name', 'Trip name is required.');
- return;
+ return false;
}
const start = parseYMD(tripForm.startDate);
@@ -197,12 +221,12 @@ export default function AppRoot() {
if (!start || !end) {
Alert.alert('Invalid dates', 'Please select valid trip dates.');
- return;
+ return false;
}
if (start > end) {
Alert.alert('Invalid dates', 'Start date cannot be after end date.');
- return;
+ return false;
}
const now = Date.now();
@@ -247,6 +271,7 @@ export default function AppRoot() {
setSelectedTripId(tripId);
setTripForm(emptyTripForm());
+ return true;
}
function setTripAsTemplate(tripId) {
@@ -401,67 +426,85 @@ export default function AppRoot() {
setCheckupSession([]);
return;
}
+ setCheckupSession(buildCheckupSession(selectedTripItems));
+ }
- const fresh = selectedTripItems.map((item) => ({
- itemId: item.id,
- name: item.name,
- category: item.category,
- current: {
- status: item.status,
- placement: item.placement,
- lentTo: item.lentTo || '',
- },
- confirmed: false,
- result: 'pending',
- }));
+ function startCheckupFlow() {
+ if (!selectedTripId) {
+ Alert.alert('No trip selected', 'Please select a trip first.');
+ return;
+ }
+ if (!selectedTripItems.length) {
+ Alert.alert('No items', 'Add items before starting a check-up.');
+ return;
+ }
+ const fresh = buildCheckupSession(selectedTripItems);
setCheckupSession(fresh);
+ setCheckupFlowIndex(0);
+ setCheckupFlowMode('question');
+ setCheckupNoForm(emptyCheckupNoForm());
+ setCheckupFlowVisible(true);
}
- function answerCheckupYes(itemId) {
- setCheckupSession((prev) =>
- prev.map((entry) => (entry.itemId === itemId ? { ...entry, confirmed: true, result: 'correct' } : entry))
- );
+ function closeCheckupFlow() {
+ setCheckupFlowVisible(false);
+ setCheckupFlowMode('question');
+ setCheckupNoForm(emptyCheckupNoForm());
}
- function openFixModal(itemId) {
- const entry = checkupSession.find((x) => x.itemId === itemId);
+ function goNextInCheckup() {
+ setCheckupFlowIndex((prev) => prev + 1);
+ setCheckupFlowMode('question');
+ setCheckupNoForm(emptyCheckupNoForm());
+ }
+
+ function answerCurrentCheckupYes() {
+ const entry = checkupCurrentEntry;
if (!entry) return;
- setCheckupFixTargetId(itemId);
- setCheckupFixForm({
+ setCheckupSession((prev) =>
+ prev.map((x) => (x.itemId === entry.itemId ? { ...x, confirmed: true, result: 'correct' } : x))
+ );
+ goNextInCheckup();
+ }
+
+ function openCurrentCheckupNo() {
+ const entry = checkupCurrentEntry;
+ if (!entry) return;
+ setCheckupNoForm({
status: entry.current.status || 'unpacked',
placement: entry.current.placement || 'suitcase',
lentTo: entry.current.lentTo || '',
updateMasterList: false,
});
- setCheckupFixModalVisible(true);
+ setCheckupFlowMode('edit');
}
- function saveFixModal() {
- if (!checkupFixTargetId) return;
+ function saveCurrentCheckupNo() {
+ const entry = checkupCurrentEntry;
+ if (!entry) return;
- const targetId = checkupFixTargetId;
const patch = {
- status: checkupFixForm.status,
- placement: checkupFixForm.placement,
- lentTo: checkupFixForm.status === 'lent-to' ? checkupFixForm.lentTo.trim() : '',
+ status: checkupNoForm.status,
+ placement: checkupNoForm.placement,
+ lentTo: checkupNoForm.status === 'lent-to' ? checkupNoForm.lentTo.trim() : '',
};
setCheckupSession((prev) =>
- prev.map((entry) =>
- entry.itemId === targetId
+ prev.map((x) =>
+ x.itemId === entry.itemId
? {
- ...entry,
+ ...x,
current: patch,
confirmed: true,
result: 'bad',
}
- : entry
+ : x
)
);
- if (checkupFixForm.updateMasterList && selectedTripId) {
+ if (checkupNoForm.updateMasterList && selectedTripId) {
setData((prev) => {
const items = prev.itemsByTrip[selectedTripId] || [];
return {
@@ -469,7 +512,7 @@ export default function AppRoot() {
itemsByTrip: {
...prev.itemsByTrip,
[selectedTripId]: items.map((item) =>
- item.id === targetId
+ item.id === entry.itemId
? {
...item,
status: patch.status,
@@ -484,28 +527,27 @@ export default function AppRoot() {
});
}
- setCheckupFixModalVisible(false);
- setCheckupFixTargetId(null);
+ goNextInCheckup();
}
- function saveCheckup() {
+ function saveCheckupSnapshot(sessionToSave) {
if (!selectedTripId) {
Alert.alert('No trip selected', 'Please select a trip first.');
- return;
+ return false;
}
- if (!checkupSession.length) {
+ if (!sessionToSave.length) {
Alert.alert('No items', 'Add items before creating a check-up.');
- return;
+ return false;
}
- const pending = checkupSession.filter((entry) => !entry.confirmed).length;
+ const pending = sessionToSave.filter((entry) => !entry.confirmed).length;
if (pending > 0) {
Alert.alert('Incomplete', `Please confirm all items first (${pending} remaining).`);
- return;
+ return false;
}
- const snapshot = checkupSession.map((entry) => ({
+ const snapshot = sessionToSave.map((entry) => ({
itemId: entry.itemId,
name: entry.name,
category: entry.category,
@@ -537,7 +579,15 @@ export default function AppRoot() {
};
});
+ return true;
+ }
+
+ function finishCheckupFlow() {
+ const ok = saveCheckupSnapshot(checkupSession);
+ if (!ok) return;
+
Alert.alert('Saved', 'Check-up snapshot saved.');
+ closeCheckupFlow();
createFreshCheckupSession();
}
@@ -592,6 +642,8 @@ export default function AppRoot() {
onInputFocus={onInputFocus}
defaultTemplateTripId={data.defaultTemplateTripId}
openDatePicker={openDatePicker}
+ activeTripItemCount={selectedTripItems.length}
+ activeTripCheckupCount={selectedTripCheckups.length}
/>
)}
@@ -608,12 +660,10 @@ export default function AppRoot() {
{tab === 'checkup' && (
)}
@@ -649,12 +699,19 @@ export default function AppRoot() {
saveItemFromModal={saveItemFromModal}
/>
-
);
diff --git a/src/modals/CheckupFlowModal.js b/src/modals/CheckupFlowModal.js
new file mode 100644
index 0000000..7254551
--- /dev/null
+++ b/src/modals/CheckupFlowModal.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import { KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
+import { ITEM_PLACEMENTS, ITEM_STATUSES } from '../constants';
+import ChipGroup from '../components/ChipGroup';
+import Field from '../components/Field';
+import { styles } from '../styles';
+
+export default function CheckupFlowModal({
+ visible,
+ entry,
+ stepIndex,
+ total,
+ mode,
+ noForm,
+ setNoForm,
+ onClose,
+ onYes,
+ onNo,
+ onSaveNo,
+ onFinish,
+}) {
+ const finished = !entry;
+
+ return (
+
+
+
+
+
+ Check-Up
+
+ Close
+
+
+
+ {finished ? (
+
+ Done. Save this snapshot?
+ All {total} items were checked.
+
+ Save Check-Up Snapshot
+
+
+ ) : (
+
+
+ Item {stepIndex + 1} / {total}
+
+ {entry.name}
+ {entry.category || 'uncategorized'}
+
+ Current: {entry.current.status} · {entry.current.placement}
+ {entry.current.status === 'lent-to' && entry.current.lentTo ? ` · ${entry.current.lentTo}` : ''}
+
+
+ {mode === 'question' ? (
+
+
+ Yes, correct
+
+
+ No, update
+
+
+ ) : (
+
+
+ setNoForm((prev) => ({ ...prev, status: v }))}
+ />
+
+
+
+ setNoForm((prev) => ({ ...prev, placement: v }))}
+ />
+
+
+ {noForm.status === 'lent-to' ? (
+
+ setNoForm((prev) => ({ ...prev, lentTo: v }))}
+ placeholder="Person name"
+ placeholderTextColor="#6b7280"
+ />
+
+ ) : null}
+
+ setNoForm((prev) => ({ ...prev, updateMasterList: !prev.updateMasterList }))}
+ >
+
+ {noForm.updateMasterList ? '☑' : '☐'} Also update item in trip list
+
+
+
+
+ Save update + next
+
+
+ )}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/styles.js b/src/styles.js
index cb100b6..32ccb9d 100644
--- a/src/styles.js
+++ b/src/styles.js
@@ -117,6 +117,32 @@ export const styles = StyleSheet.create({
marginTop: -2,
marginBottom: 2,
},
+ tripHeroCard: {
+ backgroundColor: '#0f172a',
+ borderRadius: 18,
+ borderWidth: 1,
+ borderColor: '#334155',
+ padding: 12,
+ gap: 8,
+ },
+ tripHeroImage: {
+ width: '100%',
+ height: 180,
+ borderRadius: 12,
+ backgroundColor: '#111827',
+ },
+ tripHeroTitle: {
+ color: '#f8fafc',
+ fontWeight: '800',
+ fontSize: 22,
+ },
+ tripListTitle: {
+ color: '#cbd5e1',
+ fontWeight: '700',
+ fontSize: 13,
+ letterSpacing: 0.4,
+ textTransform: 'uppercase',
+ },
fieldWrap: {
gap: 6,
@@ -323,6 +349,28 @@ export const styles = StyleSheet.create({
color: '#f8fafc',
fontWeight: '700',
},
+ answerRowWide: {
+ marginTop: 14,
+ gap: 10,
+ },
+ answerYesWide: {
+ backgroundColor: '#163223',
+ borderWidth: 1,
+ borderColor: '#1f7a4e',
+ borderRadius: 10,
+ paddingHorizontal: 16,
+ paddingVertical: 11,
+ alignItems: 'center',
+ },
+ answerNoWide: {
+ backgroundColor: '#3b1d22',
+ borderWidth: 1,
+ borderColor: '#7f1d1d',
+ borderRadius: 10,
+ paddingHorizontal: 16,
+ paddingVertical: 11,
+ alignItems: 'center',
+ },
answerStateDot: {
width: 10,
height: 10,
diff --git a/src/tabs/CheckupTab.js b/src/tabs/CheckupTab.js
index af6e2c6..0d3cd7e 100644
--- a/src/tabs/CheckupTab.js
+++ b/src/tabs/CheckupTab.js
@@ -2,71 +2,36 @@ import React from 'react';
import { Pressable, Text, View } from 'react-native';
import { styles } from '../styles';
-export default function CheckupTab({
- checkupSession,
- checkupStats,
- answerCheckupYes,
- openFixModal,
- createFreshCheckupSession,
- saveCheckup,
-}) {
+export default function CheckupTab({ selectedTrip, selectedTripItems, checkupStats, startCheckupFlow }) {
return (
-
- Check-Up
-
- Check-up
-
-
+ Check-Up
- {!!checkupSession.length && (
-
-
- Correct: {checkupStats.correct}
-
-
- Bad: {checkupStats.bad}
-
-
- Pending: {checkupStats.pending}
+ {!selectedTrip ? Select a trip first. : null}
+ {selectedTrip && selectedTripItems.length === 0 ? No items for this trip yet. : null}
+
+ {selectedTrip && selectedTripItems.length > 0 ? (
+
+ Run a check-up for {selectedTrip.name}
+ {selectedTripItems.length} items will be checked one by one.
+
+
+
+ Correct: {checkupStats.correct}
+
+
+ Bad: {checkupStats.bad}
+
+
+ Pending: {checkupStats.pending}
+
+
+
+ Start Check-Up
+
- )}
-
- {checkupSession.length === 0 ? No items for this trip yet. : null}
-
- {checkupSession.map((entry) => (
-
- {entry.name}
- {entry.category || 'uncategorized'}
-
- {entry.current.status} · {entry.current.placement}
- {entry.current.status === 'lent-to' && entry.current.lentTo ? ` · ${entry.current.lentTo}` : ''}
-
-
-
- answerCheckupYes(entry.itemId)}>
- Yes
-
- openFixModal(entry.itemId)}>
- No
-
-
-
-
- ))}
-
- {!!checkupSession.length && (
-
- Save Check-Up Snapshot
-
- )}
+ ) : null}
);
}
diff --git a/src/tabs/TripsTab.js b/src/tabs/TripsTab.js
index 542f6d1..53a9eee 100644
--- a/src/tabs/TripsTab.js
+++ b/src/tabs/TripsTab.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { Image, Pressable, Text, TextInput, View } from 'react-native';
+import React, { useMemo, useState } from 'react';
+import { Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, Text, TextInput, View } from 'react-native';
import Field from '../components/Field';
import { styles } from '../styles';
@@ -28,65 +28,40 @@ export default function TripsTab({
onInputFocus,
defaultTemplateTripId,
openDatePicker,
+ activeTripItemCount,
+ activeTripCheckupCount,
}) {
+ const [createModalVisible, setCreateModalVisible] = useState(false);
+
+ const activeTrip = useMemo(() => trips.find((trip) => trip.id === selectedTripId) || null, [trips, selectedTripId]);
+
+ function submitCreateTrip() {
+ const ok = createTrip();
+ if (ok) setCreateModalVisible(false);
+ }
+
return (
- Trips
-
-
-
- updateTripForm('name', v)}
- placeholder="Summer Weekend"
- placeholderTextColor="#6b7280"
- onFocus={onInputFocus}
- />
-
-
-
- updateTripForm('location', v)}
- placeholder="Berlin"
- placeholderTextColor="#6b7280"
- onFocus={onInputFocus}
- />
-
-
- openDatePicker('startDate')} />
- openDatePicker('endDate')} />
-
-
-
- Take photo
-
-
- {tripForm.imageUri ? 'From gallery (change)' : 'From gallery'}
-
-
-
- {tripForm.imageUri ? : null}
-
- {templateTrip ? (
- updateTripForm('copyDefaultTemplate', !tripForm.copyDefaultTemplate)}>
-
- {tripForm.copyDefaultTemplate ? '☑' : '☐'} Copy items from template ({templateTrip.name})
-
-
- ) : null}
-
- updateTripForm('setAsDefaultTemplate', !tripForm.setAsDefaultTemplate)}>
- {tripForm.setAsDefaultTemplate ? '☑' : '☐'} Set as default template
-
-
-
- Create Trip
+
+ Trips
+ setCreateModalVisible(true)}>
+ + New Trip
+ {activeTrip ? (
+
+ {activeTrip.imageUri ? : null}
+ {activeTrip.name}
+ {activeTrip.location || 'No location'} · {activeTrip.startDate} → {activeTrip.endDate}
+ {activeTripItemCount} items · {activeTripCheckupCount} check-ups
+
+ ) : (
+ Create your first trip to get started.
+ )}
+
+ All Trips
+
{trips
.slice()
.sort((a, b) => b.startDate.localeCompare(a.startDate))
@@ -113,6 +88,80 @@ export default function TripsTab({
{trip.imageUri ? : null}
))}
+
+
+
+
+
+
+ Create Trip
+ setCreateModalVisible(false)}>
+ Close
+
+
+
+
+
+ updateTripForm('name', v)}
+ placeholder="Summer Weekend"
+ placeholderTextColor="#6b7280"
+ onFocus={onInputFocus}
+ />
+
+
+
+ updateTripForm('location', v)}
+ placeholder="Berlin"
+ placeholderTextColor="#6b7280"
+ onFocus={onInputFocus}
+ />
+
+
+ openDatePicker('startDate')} />
+ openDatePicker('endDate')} />
+
+
+
+ Take photo
+
+
+ {tripForm.imageUri ? 'From gallery (change)' : 'From gallery'}
+
+
+
+ {tripForm.imageUri ? : null}
+
+ {templateTrip ? (
+ updateTripForm('copyDefaultTemplate', !tripForm.copyDefaultTemplate)}>
+
+ {tripForm.copyDefaultTemplate ? '☑' : '☐'} Copy items from template ({templateTrip.name})
+
+
+ ) : null}
+
+ updateTripForm('setAsDefaultTemplate', !tripForm.setAsDefaultTemplate)}>
+ {tripForm.setAsDefaultTemplate ? '☑' : '☐'} Set as default template
+
+
+
+ Create Trip
+
+
+
+
+
+
);
}