diff --git a/TODO.md b/TODO.md index 8f72628..d02ce56 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ # TODO - Luggage List ## Stage -Improving & Fixing Bugs (V2) +Improving & Fixing Bugs (V3) ## V2 Changes Requested - [x] Trip can be selected from everywhere (global trip picker) @@ -29,3 +29,8 @@ Improving & Fixing Bugs (V2) - [x] V2 redesign + behavior fixes implemented - [x] Removed legacy template src folder - [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 diff --git a/src/AppRoot.js b/src/AppRoot.js index a0ab718..337f7b0 100644 --- a/src/AppRoot.js +++ b/src/AppRoot.js @@ -5,6 +5,7 @@ import * as ImagePicker from 'expo-image-picker'; import { StatusBar } from 'expo-status-bar'; 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 TripsTab from './tabs/TripsTab'; @@ -45,6 +46,7 @@ export default function AppRoot() { const [selectedTripId, setSelectedTripId] = useState(null); const [tripForm, setTripForm] = useState(emptyTripForm()); + const [datePicker, setDatePicker] = useState({ visible: false, field: 'startDate' }); const [itemModalVisible, setItemModalVisible] = useState(false); const [itemForm, setItemForm] = useState(emptyItemForm()); @@ -130,6 +132,15 @@ export default function AppRoot() { 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) { const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!perm.granted) { @@ -158,7 +169,7 @@ export default function AppRoot() { const end = parseYMD(tripForm.endDate); if (!start || !end) { - Alert.alert('Invalid dates', 'Use YYYY-MM-DD format.'); + Alert.alert('Invalid dates', 'Please select valid trip dates.'); return; } @@ -456,16 +467,21 @@ export default function AppRoot() { createFreshCheckupSession(); } - function focusToEnd() { + function onInputFocus(event) { + const target = event?.target; + if (!target) return; setTimeout(() => { - scrollRef.current?.scrollToEnd?.({ animated: true }); + const scrollFn = scrollRef.current?.scrollResponderScrollNativeHandleToKeyboard; + if (typeof scrollFn === 'function') { + scrollFn(target, 90, true); + } }, 80); } if (!loaded) { return ( - + Loading local data... @@ -475,7 +491,7 @@ export default function AppRoot() { return ( - + + {tab === 'trips' && ( @@ -498,8 +515,9 @@ export default function AppRoot() { chooseTrip={setSelectedTripId} setTripAsTemplate={setTripAsTemplate} deleteTrip={deleteTrip} - focusToEnd={focusToEnd} + onInputFocus={onInputFocus} defaultTemplateTripId={data.defaultTemplateTripId} + openDatePicker={openDatePicker} /> )} @@ -525,6 +543,7 @@ export default function AppRoot() { {tab === 'history' && ( + setDatePicker((prev) => ({ ...prev, visible: false }))} + onSelect={onSelectDate} + /> + onChange(tab.key)} style={styles.tabItem}> - + {tab.label} ); diff --git a/src/components/DatePickerModal.js b/src/components/DatePickerModal.js new file mode 100644 index 0000000..e19fb65 --- /dev/null +++ b/src/components/DatePickerModal.js @@ -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 ( + + + + + {title} + + Close + + + + + goMonth(-1)}> + + + {monthLabel(viewDate)} + goMonth(1)}> + + + + + + {WEEKDAYS.map((w) => ( + {w} + ))} + + + + {grid.map((cell, idx) => { + if (!cell) return ; + const ymd = toYMD(cell); + const isSelected = ymd === selected; + return ( + onSelect(ymd)}> + {cell.getDate()} + + ); + })} + + + + + ); +} diff --git a/src/styles.js b/src/styles.js index 59530e8..a8981d3 100644 --- a/src/styles.js +++ b/src/styles.js @@ -11,10 +11,13 @@ export const styles = StyleSheet.create({ }, content: { paddingHorizontal: 14, - paddingTop: 12, - paddingBottom: TAB_BAR_HEIGHT + 18, + paddingTop: 10, + paddingBottom: TAB_BAR_HEIGHT + 20, gap: 12, }, + statusSpacer: { + height: Platform.OS === 'android' ? 8 : 0, + }, center: { flex: 1, alignItems: 'center', @@ -108,6 +111,12 @@ export const styles = StyleSheet.create({ marginTop: 3, fontSize: 13, }, + tripHistoryLabel: { + color: '#93c5fd', + fontSize: 13, + marginTop: -2, + marginBottom: 2, + }, fieldWrap: { gap: 6, @@ -125,6 +134,18 @@ export const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 11, }, + dateInput: { + borderWidth: 1, + borderColor: '#29415e', + borderRadius: 10, + backgroundColor: '#0b1220', + paddingHorizontal: 12, + paddingVertical: 12, + }, + dateInputText: { + color: '#dbeafe', + fontWeight: '600', + }, chipGroup: { flexDirection: 'row', @@ -331,16 +352,9 @@ export const styles = StyleSheet.create({ }, tabItem: { alignItems: 'center', + justifyContent: 'center', gap: 4, - }, - tabDot: { - width: 6, - height: 6, - borderRadius: 99, - backgroundColor: '#334155', - }, - tabDotActive: { - backgroundColor: '#60a5fa', + minWidth: 62, }, tabLabel: { color: '#94a3b8', @@ -373,4 +387,76 @@ export const styles = StyleSheet.create({ color: '#93c5fd', 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', + }, }); diff --git a/src/tabs/HistoryTab.js b/src/tabs/HistoryTab.js index 4d77871..9446bb6 100644 --- a/src/tabs/HistoryTab.js +++ b/src/tabs/HistoryTab.js @@ -2,11 +2,14 @@ import React from 'react'; import { Pressable, Text, View } from 'react-native'; import { styles } from '../styles'; -export default function HistoryTab({ selectedTripCheckups, selectedCheckupId, setSelectedCheckupId }) { +export default function HistoryTab({ selectedTrip, selectedTripCheckups, selectedCheckupId, setSelectedCheckupId }) { return ( History - {selectedTripCheckups.length === 0 ? No check-ups saved yet. : null} + + {!selectedTrip ? Select a trip first. : null} + {selectedTrip ? Check-ups for: {selectedTrip.name} : null} + {selectedTrip && selectedTripCheckups.length === 0 ? No check-ups saved yet. : null} {selectedTripCheckups.map((checkup) => ( diff --git a/src/tabs/TripsTab.js b/src/tabs/TripsTab.js index b288137..e509b49 100644 --- a/src/tabs/TripsTab.js +++ b/src/tabs/TripsTab.js @@ -3,6 +3,16 @@ import { Image, Pressable, Text, TextInput, View } from 'react-native'; import Field from '../components/Field'; import { styles } from '../styles'; +function DateField({ label, value, onPress }) { + return ( + + + {value} + + + ); +} + export default function TripsTab({ tripForm, updateTripForm, @@ -14,8 +24,9 @@ export default function TripsTab({ chooseTrip, setTripAsTemplate, deleteTrip, - focusToEnd, + onInputFocus, defaultTemplateTripId, + openDatePicker, }) { return ( @@ -29,7 +40,7 @@ export default function TripsTab({ onChangeText={(v) => updateTripForm('name', v)} placeholder="Summer Weekend" placeholderTextColor="#6b7280" - onFocus={focusToEnd} + onFocus={onInputFocus} /> @@ -40,29 +51,12 @@ export default function TripsTab({ onChangeText={(v) => updateTripForm('location', v)} placeholder="Berlin" placeholderTextColor="#6b7280" - onFocus={focusToEnd} + onFocus={onInputFocus} /> - - updateTripForm('startDate', v)} - placeholderTextColor="#6b7280" - onFocus={focusToEnd} - /> - - - - updateTripForm('endDate', v)} - placeholderTextColor="#6b7280" - onFocus={focusToEnd} - /> - + openDatePicker('startDate')} /> + openDatePicker('endDate')} /> {tripForm.imageUri ? 'Change trip image' : 'Add trip image'}