2 Commits

Author SHA1 Message Date
f34ffe39c0 feat: polish UI, fix top inset, and add check-up correctness stats
All checks were successful
Luggage List Build / build-web (push) Successful in 38s
Luggage List Build / build-android (push) Successful in 6m16s
Luggage List Build / release (push) Successful in 14s
2026-04-18 13:23:19 +02:00
30ee53fe75 feat: add calendar date picker, nav icons, and input-focused keyboard scrolling
All checks were successful
Luggage List Build / build-web (push) Successful in 28s
Luggage List Build / build-android (push) Successful in 5m59s
Luggage List Build / release (push) Successful in 11s
2026-04-18 13:12:51 +02:00
9 changed files with 359 additions and 62 deletions

View File

@@ -5,7 +5,7 @@ Minimal local-first luggage management app built with Expo.
## Current Features (V2) ## Current Features (V2)
- No auth, no server, local storage only (AsyncStorage) - No auth, no server, local storage only (AsyncStorage)
- Trips with name, location, dates, optional image from gallery - Trips with name, location, calendar date picker, optional image from gallery
- Active trip auto-select on first load, with manual trip switching anytime via global trip picker - Active trip auto-select on first load, with manual trip switching anytime via global trip picker
- Default trip template (copied into new trip, not linked) - Default trip template (copied into new trip, not linked)
- Luggage items with: - Luggage items with:
@@ -14,10 +14,10 @@ Minimal local-first luggage management app built with Expo.
- placement: suitcase, backpack, with-user, other - placement: suitcase, backpack, with-user, other
- optional image from gallery - optional image from gallery
- Item create/edit via modal - Item create/edit via modal
- Check-up flow as yes/no checklist: - Check-up flow as yes/no checklist with live stats (correct/bad/pending):
- “No” opens update modal - “No” opens update modal
- fixes can be check-up-only or optionally synced to trip item list - fixes can be check-up-only or optionally synced to trip item list
- Check-up history per trip with saved snapshots - Check-up history per selected trip with saved snapshots + stats
## Notes ## Notes

10
TODO.md
View File

@@ -1,7 +1,7 @@
# TODO - Luggage List # TODO - Luggage List
## Stage ## Stage
Improving & Fixing Bugs (V2) Improving & Fixing Bugs (V3)
## V2 Changes Requested ## V2 Changes Requested
- [x] Trip can be selected from everywhere (global trip picker) - [x] Trip can be selected from everywhere (global trip picker)
@@ -29,3 +29,11 @@ Improving & Fixing Bugs (V2)
- [x] V2 redesign + behavior fixes implemented - [x] V2 redesign + behavior fixes implemented
- [x] Removed legacy template src folder - [x] Removed legacy template src folder
- [x] Rebuilt app into modular `src/` structure (tabs/components/modals/styles/utils) - [x] Rebuilt app into modular `src/` structure (tabs/components/modals/styles/utils)
- [x] Fixed status-bar overlap with top spacing
- [x] Replaced trip date text inputs with calendar modal picker
- [x] Improved keyboard focus scrolling to focused input (not scroll-to-end)
- [x] Reworked bottom nav to real icons + labels
- [x] Clarified history as selected-trip check-up history
- [x] Increased safe top inset to avoid status-bar overlap
- [x] Added check-up stats (correct/bad/pending) and persisted per snapshot
- [x] Extra UI polish pass (spacing, cards, hierarchy)

View File

@@ -1,10 +1,11 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Alert, KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, Text, View } from 'react-native'; import { Alert, KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, StatusBar as RNStatusBar, Text, View } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import BottomTab from './components/BottomTab'; import BottomTab from './components/BottomTab';
import TripPicker from './components/TripPicker'; import TripPicker from './components/TripPicker';
import DatePickerModal from './components/DatePickerModal';
import ItemModal from './modals/ItemModal'; import ItemModal from './modals/ItemModal';
import CheckupFixModal from './modals/CheckupFixModal'; import CheckupFixModal from './modals/CheckupFixModal';
import TripsTab from './tabs/TripsTab'; import TripsTab from './tabs/TripsTab';
@@ -45,6 +46,7 @@ export default function AppRoot() {
const [selectedTripId, setSelectedTripId] = useState(null); const [selectedTripId, setSelectedTripId] = useState(null);
const [tripForm, setTripForm] = useState(emptyTripForm()); const [tripForm, setTripForm] = useState(emptyTripForm());
const [datePicker, setDatePicker] = useState({ visible: false, field: 'startDate' });
const [itemModalVisible, setItemModalVisible] = useState(false); const [itemModalVisible, setItemModalVisible] = useState(false);
const [itemForm, setItemForm] = useState(emptyItemForm()); const [itemForm, setItemForm] = useState(emptyItemForm());
@@ -61,6 +63,8 @@ export default function AppRoot() {
const [selectedCheckupId, setSelectedCheckupId] = useState(null); const [selectedCheckupId, setSelectedCheckupId] = useState(null);
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 selectedTrip = useMemo(() => data.trips.find((trip) => trip.id === selectedTripId) || null, [data.trips, selectedTripId]);
const selectedTripItems = useMemo(() => { const selectedTripItems = useMemo(() => {
@@ -78,6 +82,14 @@ export default function AppRoot() {
[data.trips, data.defaultTemplateTripId] [data.trips, data.defaultTemplateTripId]
); );
const checkupStats = useMemo(() => {
const total = checkupSession.length;
const correct = checkupSession.filter((entry) => entry.result === 'correct').length;
const bad = checkupSession.filter((entry) => entry.result === 'bad').length;
const pending = total - correct - bad;
return { total, correct, bad, pending };
}, [checkupSession]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
@@ -130,6 +142,15 @@ export default function AppRoot() {
setItemForm((prev) => ({ ...prev, [field]: value })); setItemForm((prev) => ({ ...prev, [field]: value }));
} }
function openDatePicker(field) {
setDatePicker({ visible: true, field });
}
function onSelectDate(ymd) {
updateTripForm(datePicker.field, ymd);
setDatePicker((prev) => ({ ...prev, visible: false }));
}
async function pickImage(onPicked) { async function pickImage(onPicked) {
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!perm.granted) { if (!perm.granted) {
@@ -158,7 +179,7 @@ export default function AppRoot() {
const end = parseYMD(tripForm.endDate); const end = parseYMD(tripForm.endDate);
if (!start || !end) { if (!start || !end) {
Alert.alert('Invalid dates', 'Use YYYY-MM-DD format.'); Alert.alert('Invalid dates', 'Please select valid trip dates.');
return; return;
} }
@@ -336,13 +357,16 @@ export default function AppRoot() {
lentTo: item.lentTo || '', lentTo: item.lentTo || '',
}, },
confirmed: false, confirmed: false,
result: 'pending',
})); }));
setCheckupSession(fresh); setCheckupSession(fresh);
} }
function answerCheckupYes(itemId) { function answerCheckupYes(itemId) {
setCheckupSession((prev) => prev.map((entry) => (entry.itemId === itemId ? { ...entry, confirmed: true } : entry))); setCheckupSession((prev) =>
prev.map((entry) => (entry.itemId === itemId ? { ...entry, confirmed: true, result: 'correct' } : entry))
);
} }
function openFixModal(itemId) { function openFixModal(itemId) {
@@ -376,6 +400,7 @@ export default function AppRoot() {
...entry, ...entry,
current: patch, current: patch,
confirmed: true, confirmed: true,
result: 'bad',
} }
: entry : entry
) )
@@ -432,6 +457,7 @@ export default function AppRoot() {
status: entry.current.status, status: entry.current.status,
placement: entry.current.placement, placement: entry.current.placement,
lentTo: entry.current.status === 'lent-to' ? entry.current.lentTo : '', lentTo: entry.current.status === 'lent-to' ? entry.current.lentTo : '',
result: entry.result || 'pending',
})); }));
setData((prev) => { setData((prev) => {
@@ -446,6 +472,10 @@ export default function AppRoot() {
id: makeId('checkup'), id: makeId('checkup'),
createdAt: Date.now(), createdAt: Date.now(),
snapshot, snapshot,
stats: {
correct: snapshot.filter((entry) => entry.result === 'correct').length,
bad: snapshot.filter((entry) => entry.result === 'bad').length,
},
}, },
], ],
}, },
@@ -456,16 +486,21 @@ export default function AppRoot() {
createFreshCheckupSession(); createFreshCheckupSession();
} }
function focusToEnd() { function onInputFocus(event) {
const target = event?.target;
if (!target) return;
setTimeout(() => { setTimeout(() => {
scrollRef.current?.scrollToEnd?.({ animated: true }); const scrollFn = scrollRef.current?.scrollResponderScrollNativeHandleToKeyboard;
if (typeof scrollFn === 'function') {
scrollFn(target, 90, true);
}
}, 80); }, 80);
} }
if (!loaded) { if (!loaded) {
return ( return (
<SafeAreaView style={styles.safe}> <SafeAreaView style={[styles.safe, { paddingTop: topInset }]}>
<StatusBar style="light" /> <StatusBar style="light" translucent={false} />
<View style={styles.center}> <View style={styles.center}>
<Text style={styles.muted}>Loading local data...</Text> <Text style={styles.muted}>Loading local data...</Text>
</View> </View>
@@ -474,8 +509,8 @@ export default function AppRoot() {
} }
return ( return (
<SafeAreaView style={styles.safe}> <SafeAreaView style={[styles.safe, { paddingTop: topInset }]}>
<StatusBar style="light" /> <StatusBar style="light" translucent={false} />
<KeyboardAvoidingView style={styles.flex} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> <KeyboardAvoidingView style={styles.flex} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<ScrollView <ScrollView
@@ -498,8 +533,9 @@ export default function AppRoot() {
chooseTrip={setSelectedTripId} chooseTrip={setSelectedTripId}
setTripAsTemplate={setTripAsTemplate} setTripAsTemplate={setTripAsTemplate}
deleteTrip={deleteTrip} deleteTrip={deleteTrip}
focusToEnd={focusToEnd} onInputFocus={onInputFocus}
defaultTemplateTripId={data.defaultTemplateTripId} defaultTemplateTripId={data.defaultTemplateTripId}
openDatePicker={openDatePicker}
/> />
)} )}
@@ -516,6 +552,7 @@ export default function AppRoot() {
{tab === 'checkup' && ( {tab === 'checkup' && (
<CheckupTab <CheckupTab
checkupSession={checkupSession} checkupSession={checkupSession}
checkupStats={checkupStats}
answerCheckupYes={answerCheckupYes} answerCheckupYes={answerCheckupYes}
openFixModal={openFixModal} openFixModal={openFixModal}
createFreshCheckupSession={createFreshCheckupSession} createFreshCheckupSession={createFreshCheckupSession}
@@ -525,6 +562,7 @@ export default function AppRoot() {
{tab === 'history' && ( {tab === 'history' && (
<HistoryTab <HistoryTab
selectedTrip={selectedTrip}
selectedTripCheckups={selectedTripCheckups} selectedTripCheckups={selectedTripCheckups}
selectedCheckupId={selectedCheckupId} selectedCheckupId={selectedCheckupId}
setSelectedCheckupId={setSelectedCheckupId} setSelectedCheckupId={setSelectedCheckupId}
@@ -535,6 +573,14 @@ export default function AppRoot() {
<BottomTab current={tab} onChange={setTab} /> <BottomTab current={tab} onChange={setTab} />
<DatePickerModal
visible={datePicker.visible}
title={datePicker.field === 'startDate' ? 'Pick start date' : 'Pick end date'}
value={tripForm[datePicker.field]}
onClose={() => setDatePicker((prev) => ({ ...prev, visible: false }))}
onSelect={onSelectDate}
/>
<ItemModal <ItemModal
visible={itemModalVisible} visible={itemModalVisible}
itemForm={itemForm} itemForm={itemForm}

View File

@@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import { Pressable, Text, View } from 'react-native'; import { Pressable, Text, View } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
import { styles } from '../styles'; import { styles } from '../styles';
export default function BottomTab({ current, onChange }) { export default function BottomTab({ current, onChange }) {
const tabs = [ const tabs = [
{ key: 'trips', label: 'Trips' }, { key: 'trips', label: 'Trips', icon: 'airplane-outline', iconActive: 'airplane' },
{ key: 'items', label: 'Items' }, { key: 'items', label: 'Items', icon: 'briefcase-outline', iconActive: 'briefcase' },
{ key: 'checkup', label: 'Check-Up' }, { key: 'checkup', label: 'Check-Up', icon: 'checkmark-circle-outline', iconActive: 'checkmark-circle' },
{ key: 'history', label: 'History' }, { key: 'history', label: 'History', icon: 'time-outline', iconActive: 'time' },
]; ];
return ( return (
@@ -17,7 +18,11 @@ export default function BottomTab({ current, onChange }) {
const active = current === tab.key; const active = current === tab.key;
return ( return (
<Pressable key={tab.key} onPress={() => onChange(tab.key)} style={styles.tabItem}> <Pressable key={tab.key} onPress={() => onChange(tab.key)} style={styles.tabItem}>
<View style={[styles.tabDot, active && styles.tabDotActive]} /> <Ionicons
name={active ? tab.iconActive : tab.icon}
size={18}
color={active ? '#dbeafe' : '#94a3b8'}
/>
<Text style={[styles.tabLabel, active && styles.tabLabelActive]}>{tab.label}</Text> <Text style={[styles.tabLabel, active && styles.tabLabelActive]}>{tab.label}</Text>
</Pressable> </Pressable>
); );

View File

@@ -0,0 +1,93 @@
import React, { useMemo, useState } from 'react';
import { Modal, Pressable, Text, View } from 'react-native';
import { styles } from '../styles';
import { todayYMD } from '../utils/date';
const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
function toYMD(date) {
const y = date.getFullYear();
const m = `${date.getMonth() + 1}`.padStart(2, '0');
const d = `${date.getDate()}`.padStart(2, '0');
return `${y}-${m}-${d}`;
}
function parseFromYMD(value) {
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return new Date();
const date = new Date(`${value}T00:00:00`);
return Number.isNaN(date.getTime()) ? new Date() : date;
}
function monthLabel(date) {
return date.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
}
function buildMonthGrid(viewDate) {
const first = new Date(viewDate.getFullYear(), viewDate.getMonth(), 1);
const startWeekday = (first.getDay() + 6) % 7;
const daysInMonth = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 0).getDate();
const cells = [];
for (let i = 0; i < startWeekday; i += 1) cells.push(null);
for (let day = 1; day <= daysInMonth; day += 1) {
cells.push(new Date(viewDate.getFullYear(), viewDate.getMonth(), day));
}
while (cells.length % 7 !== 0) cells.push(null);
return cells;
}
export default function DatePickerModal({ visible, value, onClose, onSelect, title = 'Pick date' }) {
const [viewDate, setViewDate] = useState(parseFromYMD(value || todayYMD()));
const grid = useMemo(() => buildMonthGrid(viewDate), [viewDate]);
const selected = value || todayYMD();
const goMonth = (diff) => {
setViewDate((prev) => new Date(prev.getFullYear(), prev.getMonth() + diff, 1));
};
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<View style={styles.dateModalBackdrop}>
<View style={styles.dateModalCard}>
<View style={styles.sectionRow}>
<Text style={styles.sectionTitle}>{title}</Text>
<Pressable onPress={onClose}>
<Text style={styles.closeText}>Close</Text>
</Pressable>
</View>
<View style={styles.calendarHeader}>
<Pressable style={styles.calendarNavBtn} onPress={() => goMonth(-1)}>
<Text style={styles.calendarNavText}></Text>
</Pressable>
<Text style={styles.calendarMonthText}>{monthLabel(viewDate)}</Text>
<Pressable style={styles.calendarNavBtn} onPress={() => goMonth(1)}>
<Text style={styles.calendarNavText}></Text>
</Pressable>
</View>
<View style={styles.calendarWeekRow}>
{WEEKDAYS.map((w) => (
<Text key={w} style={styles.calendarWeekday}>{w}</Text>
))}
</View>
<View style={styles.calendarGrid}>
{grid.map((cell, idx) => {
if (!cell) return <View key={`empty-${idx}`} style={styles.calendarCell} />;
const ymd = toYMD(cell);
const isSelected = ymd === selected;
return (
<Pressable key={ymd} style={[styles.calendarCell, isSelected && styles.calendarCellActive]} onPress={() => onSelect(ymd)}>
<Text style={[styles.calendarCellText, isSelected && styles.calendarCellTextActive]}>{cell.getDate()}</Text>
</Pressable>
);
})}
</View>
</View>
</View>
</Modal>
);
}

View File

@@ -10,10 +10,13 @@ export const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
content: { content: {
paddingHorizontal: 14, paddingHorizontal: 16,
paddingTop: 12, paddingTop: 12,
paddingBottom: TAB_BAR_HEIGHT + 18, paddingBottom: TAB_BAR_HEIGHT + 22,
gap: 12, gap: 14,
},
statusSpacer: {
height: Platform.OS === 'android' ? 8 : 0,
}, },
center: { center: {
flex: 1, flex: 1,
@@ -61,7 +64,7 @@ export const styles = StyleSheet.create({
}, },
section: { section: {
gap: 10, gap: 12,
}, },
sectionTitle: { sectionTitle: {
color: '#f1f5f9', color: '#f1f5f9',
@@ -77,7 +80,7 @@ export const styles = StyleSheet.create({
card: { card: {
backgroundColor: '#111827', backgroundColor: '#111827',
borderRadius: 14, borderRadius: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#1f2937', borderColor: '#1f2937',
padding: 12, padding: 12,
@@ -88,7 +91,7 @@ export const styles = StyleSheet.create({
}, },
cardSoft: { cardSoft: {
backgroundColor: '#0f172a', backgroundColor: '#0f172a',
borderRadius: 14, borderRadius: 16,
borderWidth: 1, borderWidth: 1,
borderColor: '#1e293b', borderColor: '#1e293b',
padding: 12, padding: 12,
@@ -108,6 +111,12 @@ export const styles = StyleSheet.create({
marginTop: 3, marginTop: 3,
fontSize: 13, fontSize: 13,
}, },
tripHistoryLabel: {
color: '#93c5fd',
fontSize: 13,
marginTop: -2,
marginBottom: 2,
},
fieldWrap: { fieldWrap: {
gap: 6, gap: 6,
@@ -125,6 +134,18 @@ export const styles = StyleSheet.create({
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 11, paddingVertical: 11,
}, },
dateInput: {
borderWidth: 1,
borderColor: '#29415e',
borderRadius: 10,
backgroundColor: '#0b1220',
paddingHorizontal: 12,
paddingVertical: 12,
},
dateInputText: {
color: '#dbeafe',
fontWeight: '600',
},
chipGroup: { chipGroup: {
flexDirection: 'row', flexDirection: 'row',
@@ -220,7 +241,7 @@ export const styles = StyleSheet.create({
}, },
itemCard: { itemCard: {
borderRadius: 14, borderRadius: 16,
backgroundColor: '#111827', backgroundColor: '#111827',
borderWidth: 1, borderWidth: 1,
borderColor: '#1f2937', borderColor: '#1f2937',
@@ -281,6 +302,38 @@ export const styles = StyleSheet.create({
answerStateDotOn: { answerStateDotOn: {
backgroundColor: '#22c55e', backgroundColor: '#22c55e',
}, },
answerStateDotBad: {
backgroundColor: '#ef4444',
},
statsRow: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
statPill: {
borderRadius: 999,
paddingVertical: 7,
paddingHorizontal: 11,
borderWidth: 1,
},
statPillCorrect: {
backgroundColor: '#163223',
borderColor: '#1f7a4e',
},
statPillBad: {
backgroundColor: '#3b1d22',
borderColor: '#7f1d1d',
},
statPillPending: {
backgroundColor: '#1f2937',
borderColor: '#334155',
},
statPillText: {
color: '#e2e8f0',
fontWeight: '700',
fontSize: 12,
},
snapshotWrap: { snapshotWrap: {
marginTop: 8, marginTop: 8,
@@ -331,16 +384,9 @@ export const styles = StyleSheet.create({
}, },
tabItem: { tabItem: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
gap: 4, gap: 4,
}, minWidth: 62,
tabDot: {
width: 6,
height: 6,
borderRadius: 99,
backgroundColor: '#334155',
},
tabDotActive: {
backgroundColor: '#60a5fa',
}, },
tabLabel: { tabLabel: {
color: '#94a3b8', color: '#94a3b8',
@@ -373,4 +419,76 @@ export const styles = StyleSheet.create({
color: '#93c5fd', color: '#93c5fd',
fontWeight: '700', fontWeight: '700',
}, },
dateModalBackdrop: {
flex: 1,
backgroundColor: 'rgba(2,6,23,0.75)',
justifyContent: 'center',
paddingHorizontal: 16,
},
dateModalCard: {
backgroundColor: '#0f172a',
borderRadius: 16,
borderWidth: 1,
borderColor: '#1e293b',
padding: 14,
gap: 10,
},
calendarHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
calendarNavBtn: {
width: 34,
height: 34,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 10,
backgroundColor: '#1e293b',
},
calendarNavText: {
color: '#dbeafe',
fontSize: 20,
fontWeight: '700',
lineHeight: 22,
},
calendarMonthText: {
color: '#f8fafc',
fontWeight: '700',
fontSize: 15,
},
calendarWeekRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
calendarWeekday: {
color: '#94a3b8',
width: '14.2%',
textAlign: 'center',
fontSize: 12,
},
calendarGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
calendarCell: {
width: '14.2%',
height: 38,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 10,
marginVertical: 2,
},
calendarCellActive: {
backgroundColor: '#2563eb',
},
calendarCellText: {
color: '#cbd5e1',
fontWeight: '600',
},
calendarCellTextActive: {
color: '#fff',
fontWeight: '700',
},
}); });

View File

@@ -2,7 +2,14 @@ import React from 'react';
import { Pressable, Text, View } from 'react-native'; import { Pressable, Text, View } from 'react-native';
import { styles } from '../styles'; import { styles } from '../styles';
export default function CheckupTab({ checkupSession, answerCheckupYes, openFixModal, createFreshCheckupSession, saveCheckup }) { export default function CheckupTab({
checkupSession,
checkupStats,
answerCheckupYes,
openFixModal,
createFreshCheckupSession,
saveCheckup,
}) {
return ( return (
<View style={styles.section}> <View style={styles.section}>
<View style={styles.sectionRow}> <View style={styles.sectionRow}>
@@ -12,6 +19,20 @@ export default function CheckupTab({ checkupSession, answerCheckupYes, openFixMo
</Pressable> </Pressable>
</View> </View>
{!!checkupSession.length && (
<View style={styles.statsRow}>
<View style={[styles.statPill, styles.statPillCorrect]}>
<Text style={styles.statPillText}>Correct: {checkupStats.correct}</Text>
</View>
<View style={[styles.statPill, styles.statPillBad]}>
<Text style={styles.statPillText}>Bad: {checkupStats.bad}</Text>
</View>
<View style={[styles.statPill, styles.statPillPending]}>
<Text style={styles.statPillText}>Pending: {checkupStats.pending}</Text>
</View>
</View>
)}
{checkupSession.length === 0 ? <Text style={styles.muted}>No items for this trip yet.</Text> : null} {checkupSession.length === 0 ? <Text style={styles.muted}>No items for this trip yet.</Text> : null}
{checkupSession.map((entry) => ( {checkupSession.map((entry) => (
@@ -30,7 +51,13 @@ export default function CheckupTab({ checkupSession, answerCheckupYes, openFixMo
<Pressable style={styles.answerNo} onPress={() => openFixModal(entry.itemId)}> <Pressable style={styles.answerNo} onPress={() => openFixModal(entry.itemId)}>
<Text style={styles.answerText}>No</Text> <Text style={styles.answerText}>No</Text>
</Pressable> </Pressable>
<View style={[styles.answerStateDot, entry.confirmed ? styles.answerStateDotOn : null]} /> <View
style={[
styles.answerStateDot,
entry.result === 'correct' ? styles.answerStateDotOn : null,
entry.result === 'bad' ? styles.answerStateDotBad : null,
]}
/>
</View> </View>
</View> </View>
))} ))}

View File

@@ -2,17 +2,23 @@ import React from 'react';
import { Pressable, Text, View } from 'react-native'; import { Pressable, Text, View } from 'react-native';
import { styles } from '../styles'; import { styles } from '../styles';
export default function HistoryTab({ selectedTripCheckups, selectedCheckupId, setSelectedCheckupId }) { export default function HistoryTab({ selectedTrip, selectedTripCheckups, selectedCheckupId, setSelectedCheckupId }) {
return ( return (
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>History</Text> <Text style={styles.sectionTitle}>History</Text>
{selectedTripCheckups.length === 0 ? <Text style={styles.muted}>No check-ups saved yet.</Text> : null}
{!selectedTrip ? <Text style={styles.muted}>Select a trip first.</Text> : null}
{selectedTrip ? <Text style={styles.tripHistoryLabel}>Check-ups for: {selectedTrip.name}</Text> : null}
{selectedTrip && selectedTripCheckups.length === 0 ? <Text style={styles.muted}>No check-ups saved yet.</Text> : null}
{selectedTripCheckups.map((checkup) => ( {selectedTripCheckups.map((checkup) => (
<View key={checkup.id} style={styles.cardSoft}> <View key={checkup.id} style={styles.cardSoft}>
<Pressable onPress={() => setSelectedCheckupId((prev) => (prev === checkup.id ? null : checkup.id))}> <Pressable onPress={() => setSelectedCheckupId((prev) => (prev === checkup.id ? null : checkup.id))}>
<Text style={styles.cardTitle}>{new Date(checkup.createdAt).toLocaleString()}</Text> <Text style={styles.cardTitle}>{new Date(checkup.createdAt).toLocaleString()}</Text>
<Text style={styles.cardMeta}>{checkup.snapshot.length} items</Text> <Text style={styles.cardMeta}>
{checkup.snapshot.length} items · correct: {checkup.stats?.correct ?? checkup.snapshot.filter((x) => x.result === 'correct').length} · bad:{' '}
{checkup.stats?.bad ?? checkup.snapshot.filter((x) => x.result === 'bad').length}
</Text>
<Text style={styles.cardMeta}>{selectedCheckupId === checkup.id ? 'Tap to collapse' : 'Tap to open'}</Text> <Text style={styles.cardMeta}>{selectedCheckupId === checkup.id ? 'Tap to collapse' : 'Tap to open'}</Text>
</Pressable> </Pressable>

View File

@@ -3,6 +3,16 @@ import { Image, Pressable, Text, TextInput, View } from 'react-native';
import Field from '../components/Field'; import Field from '../components/Field';
import { styles } from '../styles'; import { styles } from '../styles';
function DateField({ label, value, onPress }) {
return (
<Field label={label}>
<Pressable style={styles.dateInput} onPress={onPress}>
<Text style={styles.dateInputText}>{value}</Text>
</Pressable>
</Field>
);
}
export default function TripsTab({ export default function TripsTab({
tripForm, tripForm,
updateTripForm, updateTripForm,
@@ -14,8 +24,9 @@ export default function TripsTab({
chooseTrip, chooseTrip,
setTripAsTemplate, setTripAsTemplate,
deleteTrip, deleteTrip,
focusToEnd, onInputFocus,
defaultTemplateTripId, defaultTemplateTripId,
openDatePicker,
}) { }) {
return ( return (
<View style={styles.section}> <View style={styles.section}>
@@ -29,7 +40,7 @@ export default function TripsTab({
onChangeText={(v) => updateTripForm('name', v)} onChangeText={(v) => updateTripForm('name', v)}
placeholder="Summer Weekend" placeholder="Summer Weekend"
placeholderTextColor="#6b7280" placeholderTextColor="#6b7280"
onFocus={focusToEnd} onFocus={onInputFocus}
/> />
</Field> </Field>
@@ -40,29 +51,12 @@ export default function TripsTab({
onChangeText={(v) => updateTripForm('location', v)} onChangeText={(v) => updateTripForm('location', v)}
placeholder="Berlin" placeholder="Berlin"
placeholderTextColor="#6b7280" placeholderTextColor="#6b7280"
onFocus={focusToEnd} onFocus={onInputFocus}
/> />
</Field> </Field>
<Field label="Start Date (YYYY-MM-DD)"> <DateField label="Start Date" value={tripForm.startDate} onPress={() => openDatePicker('startDate')} />
<TextInput <DateField label="End Date" value={tripForm.endDate} onPress={() => openDatePicker('endDate')} />
style={styles.input}
value={tripForm.startDate}
onChangeText={(v) => updateTripForm('startDate', v)}
placeholderTextColor="#6b7280"
onFocus={focusToEnd}
/>
</Field>
<Field label="End Date (YYYY-MM-DD)">
<TextInput
style={styles.input}
value={tripForm.endDate}
onChangeText={(v) => updateTripForm('endDate', v)}
placeholderTextColor="#6b7280"
onFocus={focusToEnd}
/>
</Field>
<Pressable style={styles.secondaryBtn} onPress={pickTripImage}> <Pressable style={styles.secondaryBtn} onPress={pickTripImage}>
<Text style={styles.secondaryBtnText}>{tripForm.imageUri ? 'Change trip image' : 'Add trip image'}</Text> <Text style={styles.secondaryBtnText}>{tripForm.imageUri ? 'Change trip image' : 'Add trip image'}</Text>