diff --git a/src/AppRoot.js b/src/AppRoot.js
index 1d92a67..17da674 100644
--- a/src/AppRoot.js
+++ b/src/AppRoot.js
@@ -1,11 +1,12 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { Alert, KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, StatusBar as RNStatusBar, Text, View } from 'react-native';
+import { 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';
import BottomTab from './components/BottomTab';
import TripPicker from './components/TripPicker';
import DatePickerModal from './components/DatePickerModal';
+import AppDialogModal from './components/AppDialogModal';
import ItemModal from './modals/ItemModal';
import CheckupFlowModal from './modals/CheckupFlowModal';
import TripsTab from './tabs/TripsTab';
@@ -80,6 +81,7 @@ export default function AppRoot() {
const [checkupNoForm, setCheckupNoForm] = useState(emptyCheckupNoForm());
const [selectedCheckupId, setSelectedCheckupId] = useState(null);
+ const [dialogState, setDialogState] = useState({ visible: false, title: '', message: '', buttons: [] });
const topInset = Platform.OS === 'android' ? (RNStatusBar.currentHeight || 0) + 10 : 0;
@@ -116,6 +118,38 @@ export default function AppRoot() {
return checkupSession[checkupFlowIndex] || null;
}, [checkupFlowVisible, checkupFlowIndex, checkupSession]);
+ function closeDialog() {
+ setDialogState((prev) => ({ ...prev, visible: false }));
+ }
+
+ function showAlert(title, message) {
+ setDialogState({
+ visible: true,
+ title,
+ message,
+ buttons: [{ text: 'OK', tone: 'primary', onPress: closeDialog }],
+ });
+ }
+
+ function showConfirm({ title, message, confirmText = 'Confirm', onConfirm, tone = 'danger' }) {
+ setDialogState({
+ visible: true,
+ title,
+ message,
+ buttons: [
+ { text: 'Cancel', tone: 'neutral', onPress: closeDialog },
+ {
+ text: confirmText,
+ tone,
+ onPress: () => {
+ closeDialog();
+ if (typeof onConfirm === 'function') onConfirm();
+ },
+ },
+ ],
+ });
+ }
+
useEffect(() => {
(async () => {
try {
@@ -125,7 +159,7 @@ export default function AppRoot() {
setData({ ...emptyData, ...parsed });
}
} catch {
- Alert.alert('Error', 'Could not load local data.');
+ showAlert('Error', 'Could not load local data.');
} finally {
setLoaded(true);
}
@@ -135,7 +169,7 @@ export default function AppRoot() {
useEffect(() => {
if (!loaded) return;
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(data)).catch(() => {
- Alert.alert('Error', 'Could not save local data.');
+ showAlert('Error', 'Could not save local data.');
});
}, [data, loaded]);
@@ -180,7 +214,7 @@ export default function AppRoot() {
async function pickImage(onPicked) {
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!perm.granted) {
- Alert.alert('Permission needed', 'Allow gallery access to select images.');
+ showAlert('Permission needed', 'Allow gallery access to select images.');
return;
}
@@ -198,7 +232,7 @@ 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.');
+ showAlert('Permission needed', 'Allow camera access to take photos.');
return;
}
@@ -214,7 +248,7 @@ export default function AppRoot() {
function createTrip() {
if (!tripForm.name.trim()) {
- Alert.alert('Missing name', 'Trip name is required.');
+ showAlert('Missing name', 'Trip name is required.');
return false;
}
@@ -222,12 +256,12 @@ export default function AppRoot() {
const end = parseYMD(tripForm.endDate);
if (!start || !end) {
- Alert.alert('Invalid dates', 'Please select valid trip dates.');
+ showAlert('Invalid dates', 'Please select valid trip dates.');
return false;
}
if (start > end) {
- Alert.alert('Invalid dates', 'Start date cannot be after end date.');
+ showAlert('Invalid dates', 'Start date cannot be after end date.');
return false;
}
@@ -283,7 +317,7 @@ export default function AppRoot() {
function saveTripEdits(tripId, patch) {
if (!patch.name.trim()) {
- Alert.alert('Missing name', 'Trip name is required.');
+ showAlert('Missing name', 'Trip name is required.');
return false;
}
@@ -291,12 +325,12 @@ export default function AppRoot() {
const end = parseYMD(patch.endDate);
if (!start || !end) {
- Alert.alert('Invalid dates', 'Please select valid trip dates.');
+ showAlert('Invalid dates', 'Please select valid trip dates.');
return false;
}
if (start > end) {
- Alert.alert('Invalid dates', 'Start date cannot be after end date.');
+ showAlert('Invalid dates', 'Start date cannot be after end date.');
return false;
}
@@ -338,30 +372,29 @@ export default function AppRoot() {
}
function deleteTrip(tripId) {
- Alert.alert('Delete trip?', 'Trip items and check-up history will also be deleted.', [
- { text: 'Cancel', style: 'cancel' },
- {
- text: 'Delete',
- style: 'destructive',
- onPress: () => {
- setData((prev) => {
- const trips = prev.trips.filter((trip) => trip.id !== tripId);
- const itemsByTrip = { ...prev.itemsByTrip };
- const checkupsByTrip = { ...prev.checkupsByTrip };
- delete itemsByTrip[tripId];
- delete checkupsByTrip[tripId];
+ showConfirm({
+ title: 'Delete trip?',
+ message: 'Trip items and check-up history will also be deleted.',
+ confirmText: 'Delete',
+ tone: 'danger',
+ onConfirm: () => {
+ setData((prev) => {
+ const trips = prev.trips.filter((trip) => trip.id !== tripId);
+ const itemsByTrip = { ...prev.itemsByTrip };
+ const checkupsByTrip = { ...prev.checkupsByTrip };
+ delete itemsByTrip[tripId];
+ delete checkupsByTrip[tripId];
- return {
- ...prev,
- trips,
- itemsByTrip,
- checkupsByTrip,
- defaultTemplateTripId: prev.defaultTemplateTripId === tripId ? null : prev.defaultTemplateTripId,
- };
- });
- },
+ return {
+ ...prev,
+ trips,
+ itemsByTrip,
+ checkupsByTrip,
+ defaultTemplateTripId: prev.defaultTemplateTripId === tripId ? null : prev.defaultTemplateTripId,
+ };
+ });
},
- ]);
+ });
}
function openAddItemModal() {
@@ -385,12 +418,12 @@ export default function AppRoot() {
function saveItemFromModal() {
if (!selectedTripId) {
- Alert.alert('No trip selected', 'Please select or create a trip first.');
+ showAlert('No trip selected', 'Please select or create a trip first.');
return;
}
if (!itemForm.name.trim()) {
- Alert.alert('Missing name', 'Item name is required.');
+ showAlert('Missing name', 'Item name is required.');
return;
}
@@ -491,17 +524,26 @@ export default function AppRoot() {
function deleteCheckup(checkupId) {
if (!selectedTripId) return;
- setData((prev) => {
- const existing = prev.checkupsByTrip[selectedTripId] || [];
- return {
- ...prev,
- checkupsByTrip: {
- ...prev.checkupsByTrip,
- [selectedTripId]: existing.filter((checkup) => checkup.id !== checkupId),
- },
- };
+
+ showConfirm({
+ title: 'Delete check-up?',
+ message: 'This snapshot will be removed from history.',
+ confirmText: 'Delete',
+ tone: 'danger',
+ onConfirm: () => {
+ setData((prev) => {
+ const existing = prev.checkupsByTrip[selectedTripId] || [];
+ return {
+ ...prev,
+ checkupsByTrip: {
+ ...prev.checkupsByTrip,
+ [selectedTripId]: existing.filter((checkup) => checkup.id !== checkupId),
+ },
+ };
+ });
+ setSelectedCheckupId((prev) => (prev === checkupId ? null : prev));
+ },
});
- setSelectedCheckupId((prev) => (prev === checkupId ? null : prev));
}
function createFreshCheckupSession() {
@@ -514,11 +556,11 @@ export default function AppRoot() {
function startCheckupFlow() {
if (!selectedTripId) {
- Alert.alert('No trip selected', 'Please select a trip first.');
+ showAlert('No trip selected', 'Please select a trip first.');
return;
}
if (!selectedTripItems.length) {
- Alert.alert('No items', 'Add items before starting a check-up.');
+ showAlert('No items', 'Add items before starting a check-up.');
return;
}
@@ -615,18 +657,18 @@ export default function AppRoot() {
function saveCheckupSnapshot(sessionToSave) {
if (!selectedTripId) {
- Alert.alert('No trip selected', 'Please select a trip first.');
+ showAlert('No trip selected', 'Please select a trip first.');
return false;
}
if (!sessionToSave.length) {
- Alert.alert('No items', 'Add items before creating a check-up.');
+ showAlert('No items', 'Add items before creating a check-up.');
return false;
}
const pending = sessionToSave.filter((entry) => !entry.confirmed).length;
if (pending > 0) {
- Alert.alert('Incomplete', `Please confirm all items first (${pending} remaining).`);
+ showAlert('Incomplete', `Please confirm all items first (${pending} remaining).`);
return false;
}
@@ -669,7 +711,7 @@ export default function AppRoot() {
const ok = saveCheckupSnapshot(checkupSession);
if (!ok) return;
- Alert.alert('Saved', 'Check-up snapshot saved.');
+ showAlert('Saved', 'Check-up snapshot saved.');
closeCheckupFlow();
createFreshCheckupSession();
}
@@ -809,6 +851,14 @@ export default function AppRoot() {
onSaveNo={saveCurrentCheckupNo}
onFinish={finishCheckupFlow}
/>
+
+
);
}
diff --git a/src/components/AppDialogModal.js b/src/components/AppDialogModal.js
new file mode 100644
index 0000000..1546945
--- /dev/null
+++ b/src/components/AppDialogModal.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import { Modal, Pressable, Text, View } from 'react-native';
+import { styles } from '../styles';
+
+export default function AppDialogModal({ visible, title, message, buttons = [], onClose }) {
+ if (!visible) return null;
+
+ const safeButtons = buttons.length ? buttons : [{ text: 'OK' }];
+
+ return (
+
+
+
+ {title || 'Notice'}
+ {!!message ? {message} : null}
+
+
+ {safeButtons.map((button, idx) => {
+ const tone = button.tone || (button.style === 'destructive' ? 'danger' : button.style === 'cancel' ? 'neutral' : 'primary');
+ return (
+
+ {button.text || 'OK'}
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/styles.js b/src/styles.js
index fd89d7d..d962a72 100644
--- a/src/styles.js
+++ b/src/styles.js
@@ -484,6 +484,59 @@ export const styles = StyleSheet.create({
backgroundColor: 'rgba(2,6,23,0.72)',
paddingHorizontal: 12,
},
+ dialogBackdrop: {
+ flex: 1,
+ backgroundColor: 'rgba(2,6,23,0.72)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 18,
+ },
+ dialogCard: {
+ width: '100%',
+ backgroundColor: '#0f172a',
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: '#1e293b',
+ padding: 14,
+ gap: 12,
+ },
+ dialogTitle: {
+ color: '#f8fafc',
+ fontWeight: '700',
+ fontSize: 17,
+ },
+ dialogMessage: {
+ color: '#cbd5e1',
+ lineHeight: 20,
+ },
+ dialogButtonsRow: {
+ flexDirection: 'row',
+ gap: 8,
+ justifyContent: 'flex-end',
+ flexWrap: 'wrap',
+ },
+ dialogBtn: {
+ borderRadius: 10,
+ paddingVertical: 9,
+ paddingHorizontal: 14,
+ borderWidth: 1,
+ },
+ dialogBtnPrimary: {
+ backgroundColor: '#2563eb',
+ borderColor: '#2563eb',
+ },
+ dialogBtnDanger: {
+ backgroundColor: '#7f1d1d',
+ borderColor: '#991b1b',
+ },
+ dialogBtnNeutral: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ },
+ dialogBtnText: {
+ color: '#f8fafc',
+ fontWeight: '700',
+ },
modalKeyboardWrap: {
flex: 1,
width: '100%',
diff --git a/src/tabs/HistoryTab.js b/src/tabs/HistoryTab.js
index d6043f8..3b63bbb 100644
--- a/src/tabs/HistoryTab.js
+++ b/src/tabs/HistoryTab.js
@@ -1,25 +1,8 @@
import React from 'react';
-import { Alert, Pressable, Text, View } from 'react-native';
+import { Pressable, Text, View } from 'react-native';
import { styles } from '../styles';
-export default function HistoryTab({
- selectedTrip,
- selectedTripCheckups,
- selectedCheckupId,
- setSelectedCheckupId,
- onDeleteCheckup,
-}) {
- function askDelete(checkup) {
- Alert.alert('Delete check-up?', 'This snapshot will be removed from history.', [
- { text: 'Cancel', style: 'cancel' },
- {
- text: 'Delete',
- style: 'destructive',
- onPress: () => onDeleteCheckup(checkup.id),
- },
- ]);
- }
-
+export default function HistoryTab({ selectedTrip, selectedTripCheckups, selectedCheckupId, setSelectedCheckupId, onDeleteCheckup }) {
return (
History
@@ -32,7 +15,7 @@ export default function HistoryTab({
setSelectedCheckupId((prev) => (prev === checkup.id ? null : checkup.id))}
- onLongPress={() => askDelete(checkup)}
+ onLongPress={() => onDeleteCheckup(checkup.id)}
delayLongPress={280}
>
{new Date(checkup.createdAt).toLocaleString()}