From f34ffe39c06739738528de9221d70dd1a01240d1 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 18 Apr 2026 13:23:19 +0200 Subject: [PATCH] feat: polish UI, fix top inset, and add check-up correctness stats --- README.md | 6 +++--- TODO.md | 3 +++ src/AppRoot.js | 29 ++++++++++++++++++++----- src/styles.js | 48 +++++++++++++++++++++++++++++++++++------- src/tabs/CheckupTab.js | 31 +++++++++++++++++++++++++-- src/tabs/HistoryTab.js | 5 ++++- 6 files changed, 103 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 89a0122..e1279fa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Minimal local-first luggage management app built with Expo. ## Current Features (V2) - 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 - Default trip template (copied into new trip, not linked) - Luggage items with: @@ -14,10 +14,10 @@ Minimal local-first luggage management app built with Expo. - placement: suitcase, backpack, with-user, other - optional image from gallery - 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 - 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 diff --git a/TODO.md b/TODO.md index d02ce56..97a3892 100644 --- a/TODO.md +++ b/TODO.md @@ -34,3 +34,6 @@ Improving & Fixing Bugs (V3) - [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) diff --git a/src/AppRoot.js b/src/AppRoot.js index 337f7b0..ab2fa93 100644 --- a/src/AppRoot.js +++ b/src/AppRoot.js @@ -1,5 +1,5 @@ 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 * as ImagePicker from 'expo-image-picker'; import { StatusBar } from 'expo-status-bar'; @@ -63,6 +63,8 @@ export default function AppRoot() { 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 selectedTripItems = useMemo(() => { @@ -80,6 +82,14 @@ export default function AppRoot() { [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(() => { (async () => { try { @@ -347,13 +357,16 @@ export default function AppRoot() { lentTo: item.lentTo || '', }, confirmed: false, + result: 'pending', })); setCheckupSession(fresh); } 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) { @@ -387,6 +400,7 @@ export default function AppRoot() { ...entry, current: patch, confirmed: true, + result: 'bad', } : entry ) @@ -443,6 +457,7 @@ export default function AppRoot() { status: entry.current.status, placement: entry.current.placement, lentTo: entry.current.status === 'lent-to' ? entry.current.lentTo : '', + result: entry.result || 'pending', })); setData((prev) => { @@ -457,6 +472,10 @@ export default function AppRoot() { id: makeId('checkup'), createdAt: Date.now(), snapshot, + stats: { + correct: snapshot.filter((entry) => entry.result === 'correct').length, + bad: snapshot.filter((entry) => entry.result === 'bad').length, + }, }, ], }, @@ -480,7 +499,7 @@ export default function AppRoot() { if (!loaded) { return ( - + Loading local data... @@ -490,7 +509,7 @@ export default function AppRoot() { } return ( - + @@ -500,7 +519,6 @@ export default function AppRoot() { keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} > - {tab === 'trips' && ( @@ -534,6 +552,7 @@ export default function AppRoot() { {tab === 'checkup' && ( @@ -12,6 +19,20 @@ export default function CheckupTab({ checkupSession, answerCheckupYes, openFixMo + {!!checkupSession.length && ( + + + Correct: {checkupStats.correct} + + + Bad: {checkupStats.bad} + + + Pending: {checkupStats.pending} + + + )} + {checkupSession.length === 0 ? No items for this trip yet. : null} {checkupSession.map((entry) => ( @@ -30,7 +51,13 @@ export default function CheckupTab({ checkupSession, answerCheckupYes, openFixMo openFixModal(entry.itemId)}> No - + ))} diff --git a/src/tabs/HistoryTab.js b/src/tabs/HistoryTab.js index 9446bb6..0d87f8d 100644 --- a/src/tabs/HistoryTab.js +++ b/src/tabs/HistoryTab.js @@ -15,7 +15,10 @@ export default function HistoryTab({ selectedTrip, selectedTripCheckups, selecte setSelectedCheckupId((prev) => (prev === checkup.id ? null : checkup.id))}> {new Date(checkup.createdAt).toLocaleString()} - {checkup.snapshot.length} items + + {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} + {selectedCheckupId === checkup.id ? 'Tap to collapse' : 'Tap to open'}