Compare commits
5 Commits
luggage-li
...
luggage-li
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e45261354 | |||
| bd500674a0 | |||
| ef7e0ba7a1 | |||
| f34ffe39c0 | |||
| 30ee53fe75 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
12
TODO.md
12
TODO.md
@@ -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,13 @@ 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)
|
||||||
|
- [x] Centered and enlarged edit/check-up modals to fully overlay nav
|
||||||
|
- [x] Fixed modal keyboard glitching (stable centered keyboard-aware layout)
|
||||||
|
|||||||
11
app.json
11
app.json
@@ -34,6 +34,15 @@
|
|||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "1275f90e-33c6-4af1-942e-ca29a309f8c8"
|
"projectId": "1275f90e-33c6-4af1-942e-ca29a309f8c8"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"expo-image-picker",
|
||||||
|
{
|
||||||
|
"photosPermission": "Allow Luggage List to access your photos for trip and item images.",
|
||||||
|
"cameraPermission": "Allow Luggage List to use your camera to take trip and item photos."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -148,6 +169,23 @@ export default function AppRoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function takeImage(onPicked) {
|
||||||
|
const perm = await ImagePicker.requestCameraPermissionsAsync();
|
||||||
|
if (!perm.granted) {
|
||||||
|
Alert.alert('Permission needed', 'Allow camera access to take photos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchCameraAsync({
|
||||||
|
allowsEditing: false,
|
||||||
|
quality: 0.85,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.assets?.[0]?.uri) {
|
||||||
|
onPicked(result.assets[0].uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createTrip() {
|
function createTrip() {
|
||||||
if (!tripForm.name.trim()) {
|
if (!tripForm.name.trim()) {
|
||||||
Alert.alert('Missing name', 'Trip name is required.');
|
Alert.alert('Missing name', 'Trip name is required.');
|
||||||
@@ -158,7 +196,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 +374,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 +417,7 @@ export default function AppRoot() {
|
|||||||
...entry,
|
...entry,
|
||||||
current: patch,
|
current: patch,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
|
result: 'bad',
|
||||||
}
|
}
|
||||||
: entry
|
: entry
|
||||||
)
|
)
|
||||||
@@ -432,6 +474,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 +489,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 +503,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 +526,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
|
||||||
@@ -491,6 +543,7 @@ export default function AppRoot() {
|
|||||||
tripForm={tripForm}
|
tripForm={tripForm}
|
||||||
updateTripForm={updateTripForm}
|
updateTripForm={updateTripForm}
|
||||||
pickTripImage={() => pickImage((uri) => updateTripForm('imageUri', uri))}
|
pickTripImage={() => pickImage((uri) => updateTripForm('imageUri', uri))}
|
||||||
|
takeTripImage={() => takeImage((uri) => updateTripForm('imageUri', uri))}
|
||||||
templateTrip={templateTrip}
|
templateTrip={templateTrip}
|
||||||
createTrip={createTrip}
|
createTrip={createTrip}
|
||||||
trips={data.trips}
|
trips={data.trips}
|
||||||
@@ -498,8 +551,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 +570,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 +580,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,12 +591,21 @@ 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}
|
||||||
setItemModalVisible={setItemModalVisible}
|
setItemModalVisible={setItemModalVisible}
|
||||||
updateItemForm={updateItemForm}
|
updateItemForm={updateItemForm}
|
||||||
pickItemImage={() => pickImage((uri) => updateItemForm('imageUri', uri))}
|
pickItemImage={() => pickImage((uri) => updateItemForm('imageUri', uri))}
|
||||||
|
takeItemImage={() => takeImage((uri) => updateItemForm('imageUri', uri))}
|
||||||
saveItemFromModal={saveItemFromModal}
|
saveItemFromModal={saveItemFromModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
93
src/components/DatePickerModal.js
Normal file
93
src/components/DatePickerModal.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export default function CheckupFixModal({
|
|||||||
return (
|
return (
|
||||||
<Modal visible={visible} animationType="slide" transparent>
|
<Modal visible={visible} animationType="slide" transparent>
|
||||||
<View style={styles.modalBackdrop}>
|
<View style={styles.modalBackdrop}>
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalKeyboardWrap}>
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.modalKeyboardWrap}>
|
||||||
<View style={styles.modalCard}>
|
<View style={styles.modalCard}>
|
||||||
<View style={styles.sectionRow}>
|
<View style={styles.sectionRow}>
|
||||||
<Text style={styles.sectionTitle}>Update for this Check-Up</Text>
|
<Text style={styles.sectionTitle}>Update for this Check-Up</Text>
|
||||||
@@ -24,7 +24,12 @@ export default function CheckupFixModal({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
<ScrollView
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="interactive"
|
||||||
|
contentContainerStyle={{ paddingBottom: 12 }}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
<Field label="Status">
|
<Field label="Status">
|
||||||
<ChipGroup
|
<ChipGroup
|
||||||
options={ITEM_STATUSES}
|
options={ITEM_STATUSES}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ export default function ItemModal({
|
|||||||
setItemModalVisible,
|
setItemModalVisible,
|
||||||
updateItemForm,
|
updateItemForm,
|
||||||
pickItemImage,
|
pickItemImage,
|
||||||
|
takeItemImage,
|
||||||
saveItemFromModal,
|
saveItemFromModal,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} animationType="slide" transparent>
|
<Modal visible={visible} animationType="slide" transparent>
|
||||||
<View style={styles.modalBackdrop}>
|
<View style={styles.modalBackdrop}>
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.modalKeyboardWrap}>
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.modalKeyboardWrap}>
|
||||||
<View style={styles.modalCard}>
|
<View style={styles.modalCard}>
|
||||||
<View style={styles.sectionRow}>
|
<View style={styles.sectionRow}>
|
||||||
<Text style={styles.sectionTitle}>{itemForm.id ? 'Update Item' : 'Add Item'}</Text>
|
<Text style={styles.sectionTitle}>{itemForm.id ? 'Update Item' : 'Add Item'}</Text>
|
||||||
@@ -25,7 +26,12 @@ export default function ItemModal({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
<ScrollView
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="interactive"
|
||||||
|
contentContainerStyle={{ paddingBottom: 12 }}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
<Field label="Name">
|
<Field label="Name">
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
@@ -76,9 +82,14 @@ export default function ItemModal({
|
|||||||
</Field>
|
</Field>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Pressable style={styles.secondaryBtn} onPress={pickItemImage}>
|
<View style={styles.actionRow}>
|
||||||
<Text style={styles.secondaryBtnText}>{itemForm.imageUri ? 'Change image' : 'Add image'}</Text>
|
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={takeItemImage}>
|
||||||
</Pressable>
|
<Text style={styles.secondaryBtnText}>Take photo</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={pickItemImage}>
|
||||||
|
<Text style={styles.secondaryBtnText}>{itemForm.imageUri ? 'From gallery (change)' : 'From gallery'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
{!!itemForm.imageUri && <Image source={{ uri: itemForm.imageUri }} style={styles.previewImageSmall} />}
|
{!!itemForm.imageUri && <Image source={{ uri: itemForm.imageUri }} style={styles.previewImageSmall} />}
|
||||||
|
|
||||||
<Pressable style={styles.primaryBtn} onPress={saveItemFromModal}>
|
<Pressable style={styles.primaryBtn} onPress={saveItemFromModal}>
|
||||||
|
|||||||
170
src/styles.js
170
src/styles.js
@@ -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',
|
||||||
@@ -190,6 +211,11 @@ export const styles = StyleSheet.create({
|
|||||||
color: '#dbeafe',
|
color: '#dbeafe',
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
actionRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
|
||||||
inlineToggle: {
|
inlineToggle: {
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
@@ -220,7 +246,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 +307,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 +389,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',
|
||||||
@@ -354,23 +405,98 @@ export const styles = StyleSheet.create({
|
|||||||
modalBackdrop: {
|
modalBackdrop: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(2,6,23,0.72)',
|
backgroundColor: 'rgba(2,6,23,0.72)',
|
||||||
justifyContent: 'flex-end',
|
paddingHorizontal: 12,
|
||||||
},
|
},
|
||||||
modalKeyboardWrap: {
|
modalKeyboardWrap: {
|
||||||
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
modalCard: {
|
modalCard: {
|
||||||
maxHeight: '87%',
|
width: '96%',
|
||||||
|
maxHeight: '90%',
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: '#0f172a',
|
||||||
borderTopLeftRadius: 18,
|
borderRadius: 20,
|
||||||
borderTopRightRadius: 18,
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#1e293b',
|
borderColor: '#1e293b',
|
||||||
padding: 14,
|
padding: 16,
|
||||||
gap: 8,
|
gap: 10,
|
||||||
},
|
},
|
||||||
closeText: {
|
closeText: {
|
||||||
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,21 @@ 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,
|
||||||
pickTripImage,
|
pickTripImage,
|
||||||
|
takeTripImage,
|
||||||
templateTrip,
|
templateTrip,
|
||||||
createTrip,
|
createTrip,
|
||||||
trips,
|
trips,
|
||||||
@@ -14,8 +25,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 +41,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,33 +52,21 @@ 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)">
|
<View style={styles.actionRow}>
|
||||||
<TextInput
|
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={takeTripImage}>
|
||||||
style={styles.input}
|
<Text style={styles.secondaryBtnText}>Take photo</Text>
|
||||||
value={tripForm.endDate}
|
</Pressable>
|
||||||
onChangeText={(v) => updateTripForm('endDate', v)}
|
<Pressable style={[styles.secondaryBtnTight, styles.flex]} onPress={pickTripImage}>
|
||||||
placeholderTextColor="#6b7280"
|
<Text style={styles.secondaryBtnText}>{tripForm.imageUri ? 'From gallery (change)' : 'From gallery'}</Text>
|
||||||
onFocus={focusToEnd}
|
</Pressable>
|
||||||
/>
|
</View>
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Pressable style={styles.secondaryBtn} onPress={pickTripImage}>
|
|
||||||
<Text style={styles.secondaryBtnText}>{tripForm.imageUri ? 'Change trip image' : 'Add trip image'}</Text>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{tripForm.imageUri ? <Image source={{ uri: tripForm.imageUri }} style={styles.previewImage} /> : null}
|
{tripForm.imageUri ? <Image source={{ uri: tripForm.imageUri }} style={styles.previewImage} /> : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user