feat: Sprint 5 - Toast, ConfirmDialog, EmptyStates, Settings, Export/Import, Accessibility (TERLOG-52/53/54/55/56/57/58/59)

This commit is contained in:
root
2026-02-19 22:05:40 +08:00
parent 6fe6ebef13
commit c8fe67f346
20 changed files with 1416 additions and 619 deletions

View File

@@ -1,12 +1,15 @@
import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
import { useLocalSearchParams, router, useFocusEffect } from 'expo-router';
import { useState, useCallback } from 'react';
import { ArrowLeft, Edit2, Trash2, MapPin } from 'lucide-react-native';
import { ArrowLeft, Edit2, Trash2 } from 'lucide-react-native';
import { getDatabase } from '@/lib/database';
import { dbToContact } from '@/lib/contactHelpers';
import { Contact } from '@/store/useContactStore';
import { EditContactSheet } from '@/components/contacts/EditContactSheet';
import { LogVisitSheet } from '@/components/visits/LogVisitSheet';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { EmptyState } from '@/components/ui/EmptyState';
import { useToast } from '@/components/ui/Toast';
import { Visit, dbToVisit, formatDate } from '@/lib/visitHelpers';
const statusColors: Record<string, string> = {
@@ -23,6 +26,8 @@ export default function ContactDetailScreen() {
const [visits, setVisits] = useState<Visit[]>([]);
const [showEdit, setShowEdit] = useState(false);
const [showLogVisit, setShowLogVisit] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const { showToast } = useToast();
async function loadData() {
const db = await getDatabase();
@@ -34,18 +39,11 @@ export default function ContactDetailScreen() {
useFocusEffect(useCallback(() => { loadData(); }, [id]));
async function handleDelete() {
Alert.alert('Delete Contact', `Are you sure you want to delete ${contact?.fullName}?`, [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete', style: 'destructive',
onPress: async () => {
const db = await getDatabase();
await db.runAsync('UPDATE contacts SET deleted_at = ? WHERE id = ?', [Math.floor(Date.now() / 1000), id]);
router.back();
}
}
]);
async function handleDeleteConfirmed() {
const db = await getDatabase();
await db.runAsync('UPDATE contacts SET deleted_at = ? WHERE id = ?', [Math.floor(Date.now() / 1000), id]);
showToast(`${contact?.fullName} deleted`, 'success');
router.back();
}
if (!contact) return <View className="flex-1 bg-secondary" />;
@@ -56,14 +54,32 @@ export default function ContactDetailScreen() {
<View className="flex-1 bg-secondary">
<View className="bg-primary pt-14 pb-6 px-4">
<View className="flex-row items-center justify-between mb-4">
<TouchableOpacity onPress={() => router.back()} className="bg-white/20 rounded-full p-2">
<TouchableOpacity
onPress={() => router.back()}
className="bg-white/20 rounded-full p-2"
accessibilityRole="button"
accessibilityLabel="Go back"
style={{ minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }}
>
<ArrowLeft size={20} color="white" />
</TouchableOpacity>
<View className="flex-row gap-2">
<TouchableOpacity onPress={() => setShowEdit(true)} className="bg-white/20 rounded-full p-2">
<TouchableOpacity
onPress={() => setShowEdit(true)}
className="bg-white/20 rounded-full p-2"
accessibilityRole="button"
accessibilityLabel="Edit contact"
style={{ minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }}
>
<Edit2 size={18} color="white" />
</TouchableOpacity>
<TouchableOpacity onPress={handleDelete} className="bg-white/20 rounded-full p-2">
<TouchableOpacity
onPress={() => setShowDeleteConfirm(true)}
className="bg-white/20 rounded-full p-2"
accessibilityRole="button"
accessibilityLabel="Delete contact"
style={{ minWidth: 44, minHeight: 44, alignItems: 'center', justifyContent: 'center' }}
>
<Trash2 size={18} color="white" />
</TouchableOpacity>
</View>
@@ -99,12 +115,22 @@ export default function ContactDetailScreen() {
<View className="bg-white rounded-2xl p-4">
<View className="flex-row justify-between items-center mb-3">
<Text className="text-charcoal font-semibold">Visit History ({visits.length})</Text>
<TouchableOpacity onPress={() => setShowLogVisit(true)} className="bg-primary rounded-lg px-3 py-1.5">
<TouchableOpacity
onPress={() => setShowLogVisit(true)}
className="bg-primary rounded-lg px-3 py-1.5"
accessibilityRole="button"
accessibilityLabel="Log a new visit"
style={{ minHeight: 36 }}
>
<Text className="text-white text-sm font-medium">+ Log Visit</Text>
</TouchableOpacity>
</View>
{visits.length === 0 ? (
<Text className="text-gray-400 text-sm">No visits recorded yet</Text>
<EmptyState
icon="Calendar"
title="No visits logged yet"
message="Tap '+ Log Visit' to record your first visit with this contact."
/>
) : (
visits.map((v) => (
<View key={v.id} className="border-l-2 border-primary pl-3 mb-3 last:mb-0">
@@ -127,6 +153,15 @@ export default function ContactDetailScreen() {
onClose={() => setShowLogVisit(false)}
onSaved={() => { setShowLogVisit(false); loadData(); }}
/>
<ConfirmDialog
visible={showDeleteConfirm}
title="Delete Contact"
message={`Are you sure you want to delete ${contact.fullName}? This action cannot be undone.`}
confirmText="Delete"
confirmStyle="danger"
onConfirm={() => { setShowDeleteConfirm(false); handleDeleteConfirmed(); }}
onCancel={() => setShowDeleteConfirm(false)}
/>
</View>
);
}