Sergei 3bc0d2a8a9 feat(device-settings): Replace Location TextInput with Picker
- Replace free-text Location input with modal Picker selector
- Use ROOM_LOCATIONS constants for predefined room options
- Show icon and label for each location option
- Highlight currently selected location in picker

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 14:21:04 -08:00

1010 lines
30 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
TextInput,
Modal,
FlatList,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router';
import * as Device from 'expo-device';
import { useBLE } from '@/contexts/BLEContext';
import { api, ROOM_LOCATIONS, type RoomLocationId } from '@/services/api';
import type { WiFiStatus } from '@/services/ble';
import {
AppColors,
BorderRadius,
FontSizes,
FontWeights,
Spacing,
Shadows,
} from '@/constants/theme';
interface SensorInfo {
deviceId: string;
wellId: number;
mac: string;
name: string;
status: 'online' | 'warning' | 'offline';
lastSeen: Date;
beneficiaryId: string;
deploymentId: number;
location?: string;
description?: string;
}
export default function DeviceSettingsScreen() {
const { id, deviceId } = useLocalSearchParams<{ id: string; deviceId: string }>();
const {
connectedDevices,
isBLEAvailable,
connectDevice,
disconnectDevice,
getCurrentWiFi,
rebootDevice,
} = useBLE();
const [sensorInfo, setSensorInfo] = useState<SensorInfo | null>(null);
const [currentWiFi, setCurrentWiFi] = useState<WiFiStatus | null>(null);
const [isLoadingInfo, setIsLoadingInfo] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
const [isLoadingWiFi, setIsLoadingWiFi] = useState(false);
const [isRebooting, setIsRebooting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// Editable fields
const [location, setLocation] = useState<RoomLocationId | ''>('');
const [description, setDescription] = useState('');
const [showLocationPicker, setShowLocationPicker] = useState(false);
const isConnected = connectedDevices.has(deviceId!);
useEffect(() => {
loadSensorInfo();
}, []);
const loadSensorInfo = async () => {
setIsLoadingInfo(true);
try {
// Get sensor info from API
const response = await api.getDevicesForBeneficiary(id!);
if (!response.ok || !response.data) {
throw new Error('Failed to load sensor info');
}
const sensor = response.data.find((s: SensorInfo) => s.deviceId === deviceId);
if (sensor) {
setSensorInfo(sensor);
setLocation((sensor.location as RoomLocationId) || '');
setDescription(sensor.description || '');
} else {
throw new Error('Sensor not found');
}
} catch (error: any) {
Alert.alert('Error', 'Failed to load sensor information');
router.back();
} finally {
setIsLoadingInfo(false);
}
};
const handleConnect = async () => {
if (!sensorInfo) return;
setIsConnecting(true);
try {
const success = await connectDevice(deviceId!);
if (!success) {
throw new Error('Connection failed');
}
// Load WiFi status after connecting
loadWiFiStatus();
} catch (error: any) {
Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.');
} finally {
setIsConnecting(false);
}
};
const loadWiFiStatus = async () => {
if (!isConnected) return;
setIsLoadingWiFi(true);
try {
const wifiStatus = await getCurrentWiFi(deviceId!);
setCurrentWiFi(wifiStatus);
} catch (error: any) {
Alert.alert('Error', 'Failed to get WiFi status');
} finally {
setIsLoadingWiFi(false);
}
};
const handleChangeWiFi = () => {
if (!isConnected) {
Alert.alert('Not Connected', 'Please connect to the sensor via Bluetooth first.');
return;
}
// Navigate to Setup WiFi screen
router.push({
pathname: `/(tabs)/beneficiaries/${id}/setup-wifi` as any,
params: {
deviceId: deviceId!,
deviceName: sensorInfo?.name || '',
wellId: sensorInfo?.wellId.toString() || '',
},
});
};
const handleReboot = () => {
if (!isConnected) {
Alert.alert('Not Connected', 'Please connect to the sensor via Bluetooth first.');
return;
}
Alert.alert(
'Reboot Sensor',
'Are you sure you want to reboot this sensor? It will disconnect from Bluetooth and restart.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reboot',
style: 'destructive',
onPress: async () => {
setIsRebooting(true);
try {
await rebootDevice(deviceId!);
Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.');
router.back();
} catch (error: any) {
Alert.alert('Error', 'Failed to reboot sensor');
} finally{
setIsRebooting(false);
}
},
},
]
);
};
const handleSaveMetadata = async () => {
if (!sensorInfo) return;
// Check if anything changed
const locationChanged = location !== (sensorInfo.location || '');
const descriptionChanged = description !== (sensorInfo.description || '');
if (!locationChanged && !descriptionChanged) {
Alert.alert('No Changes', 'No changes to save.');
return;
}
setIsSaving(true);
try {
const updates: { location?: string; description?: string } = {};
if (locationChanged) updates.location = location;
if (descriptionChanged) updates.description = description;
const response = await api.updateDeviceMetadata(sensorInfo.deviceId, updates);
if (!response.ok) {
throw new Error(response.error?.message || 'Failed to save');
}
// Update local state
setSensorInfo({
...sensorInfo,
location,
description,
});
Alert.alert('Success', 'Device information updated.');
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to save device information');
} finally {
setIsSaving(false);
}
};
const hasUnsavedChanges = () => {
if (!sensorInfo) return false;
return (
location !== (sensorInfo.location || '') ||
description !== (sensorInfo.description || '')
);
};
const formatLastSeen = (lastSeen: Date): string => {
const now = new Date();
const diffMs = now.getTime() - lastSeen.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
};
const getSignalStrength = (rssi: number): string => {
if (rssi >= -50) return 'Excellent';
if (rssi >= -60) return 'Good';
if (rssi >= -70) return 'Fair';
return 'Weak';
};
const getSignalColor = (rssi: number) => {
if (rssi >= -50) return AppColors.success;
if (rssi >= -60) return AppColors.info;
if (rssi >= -70) return AppColors.warning;
return AppColors.error;
};
if (isLoadingInfo || !sensorInfo) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Sensor Settings</Text>
<View style={styles.placeholder} />
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading sensor info...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Sensor Settings</Text>
<View style={styles.placeholder} />
</View>
{/* Simulator Warning */}
{!isBLEAvailable && (
<View style={styles.simulatorWarning}>
<Ionicons name="information-circle" size={18} color={AppColors.warning} />
<Text style={styles.simulatorWarningText}>
Running in Simulator - BLE features use mock data
</Text>
</View>
)}
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
{/* Sensor Info Card */}
<View style={styles.sensorCard}>
<View style={styles.sensorIcon}>
<Ionicons name="water" size={48} color={AppColors.primary} />
</View>
<View style={styles.sensorInfo}>
<Text style={styles.sensorName}>{sensorInfo.name}</Text>
<View style={styles.statusRow}>
<View
style={[
styles.statusDot,
{
backgroundColor:
sensorInfo.status === 'online' ? AppColors.success : AppColors.error,
},
]}
/>
<Text style={styles.statusText}>
{sensorInfo.status === 'online' ? 'Online' : 'Offline'}
</Text>
<Text style={styles.statusSeparator}></Text>
<Text style={styles.lastSeenText}>{formatLastSeen(sensorInfo.lastSeen)}</Text>
</View>
</View>
</View>
{/* Details Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Device Information</Text>
<View style={styles.detailsCard}>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Well ID</Text>
<Text style={styles.detailValue}>{sensorInfo.wellId}</Text>
</View>
<View style={styles.detailDivider} />
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>MAC Address</Text>
<Text style={styles.detailValue}>{sensorInfo.mac}</Text>
</View>
<View style={styles.detailDivider} />
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Deployment ID</Text>
<Text style={styles.detailValue}>{sensorInfo.deploymentId}</Text>
</View>
</View>
</View>
{/* Editable Metadata Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Sensor Details</Text>
<View style={styles.detailsCard}>
<View style={styles.editableRow}>
<Text style={styles.editableLabel}>Location</Text>
<TouchableOpacity
style={styles.pickerButton}
onPress={() => setShowLocationPicker(true)}
>
<Text style={[
styles.pickerButtonText,
!location && styles.pickerButtonPlaceholder
]}>
{location
? `${ROOM_LOCATIONS.find(l => l.id === location)?.icon} ${ROOM_LOCATIONS.find(l => l.id === location)?.label}`
: 'Select location...'}
</Text>
<Ionicons name="chevron-down" size={20} color={AppColors.textMuted} />
</TouchableOpacity>
</View>
<View style={styles.detailDivider} />
<View style={styles.editableRow}>
<Text style={styles.editableLabel}>Description</Text>
<TextInput
style={[styles.editableInput, styles.editableInputMultiline]}
value={description}
onChangeText={setDescription}
placeholder="Add notes about this sensor..."
placeholderTextColor={AppColors.textMuted}
multiline
numberOfLines={2}
/>
</View>
{hasUnsavedChanges() && (
<>
<View style={styles.detailDivider} />
<TouchableOpacity
style={styles.saveButton}
onPress={handleSaveMetadata}
disabled={isSaving}
>
{isSaving ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Ionicons name="checkmark" size={20} color={AppColors.white} />
)}
<Text style={styles.saveButtonText}>
{isSaving ? 'Saving...' : 'Save Changes'}
</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
{/* BLE Connection Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Bluetooth Connection</Text>
{isConnected ? (
<View style={styles.connectedCard}>
<View style={styles.connectedHeader}>
<Ionicons name="bluetooth" size={24} color={AppColors.success} />
<Text style={styles.connectedText}>Connected</Text>
</View>
<TouchableOpacity
style={styles.disconnectButton}
onPress={() => disconnectDevice(deviceId!)}
>
<Text style={styles.disconnectButtonText}>Disconnect</Text>
</TouchableOpacity>
</View>
) : (
<TouchableOpacity
style={styles.connectButton}
onPress={handleConnect}
disabled={isConnecting}
>
{isConnecting ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Ionicons name="bluetooth" size={20} color={AppColors.white} />
)}
<Text style={styles.connectButtonText}>
{isConnecting ? 'Connecting...' : 'Connect via Bluetooth'}
</Text>
</TouchableOpacity>
)}
</View>
{/* WiFi Status Section */}
{isConnected && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>WiFi Status</Text>
{isLoadingWiFi ? (
<View style={styles.loadingWiFiCard}>
<ActivityIndicator size="small" color={AppColors.primary} />
<Text style={styles.loadingWiFiText}>Loading WiFi status...</Text>
</View>
) : currentWiFi ? (
<View style={styles.wifiCard}>
<View style={styles.wifiHeader}>
<Ionicons name="wifi" size={24} color={AppColors.success} />
<View style={styles.wifiInfo}>
<Text style={styles.wifiSSID}>{currentWiFi.ssid}</Text>
<Text
style={[styles.wifiSignal, { color: getSignalColor(currentWiFi.rssi) }]}
>
{getSignalStrength(currentWiFi.rssi)} ({currentWiFi.rssi} dBm)
</Text>
</View>
</View>
<TouchableOpacity style={styles.changeWiFiButton} onPress={handleChangeWiFi}>
<Ionicons name="settings-outline" size={18} color={AppColors.primary} />
<Text style={styles.changeWiFiText}>Change WiFi</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.noWiFiCard}>
<Ionicons name="wifi-outline" size={32} color={AppColors.textMuted} />
<Text style={styles.noWiFiText}>Not connected to WiFi</Text>
<TouchableOpacity style={styles.setupWiFiButton} onPress={handleChangeWiFi}>
<Text style={styles.setupWiFiText}>Setup WiFi</Text>
</TouchableOpacity>
</View>
)}
</View>
)}
{/* Actions Section */}
{isConnected && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Actions</Text>
<View style={styles.actionsCard}>
<TouchableOpacity
style={styles.actionButton}
onPress={loadWiFiStatus}
disabled={isLoadingWiFi}
>
<Ionicons name="refresh" size={20} color={AppColors.primary} />
<Text style={styles.actionButtonText}>Refresh WiFi Status</Text>
</TouchableOpacity>
<View style={styles.actionDivider} />
<TouchableOpacity
style={styles.actionButton}
onPress={handleReboot}
disabled={isRebooting}
>
{isRebooting ? (
<ActivityIndicator size="small" color={AppColors.warning} />
) : (
<Ionicons name="power" size={20} color={AppColors.warning} />
)}
<Text style={[styles.actionButtonText, { color: AppColors.warning }]}>
Reboot Sensor
</Text>
</TouchableOpacity>
</View>
</View>
)}
{/* Info Card */}
<View style={styles.infoCard}>
<View style={styles.infoHeader}>
<Ionicons name="information-circle" size={20} color={AppColors.info} />
<Text style={styles.infoTitle}>About Settings</Text>
</View>
<Text style={styles.infoText}>
Connect via Bluetooth to view WiFi status and change settings{'\n'}
Make sure you're within range (about 10 meters) of the sensor{'\n'}
• Rebooting will disconnect Bluetooth and restart the sensor
</Text>
</View>
</ScrollView>
{/* Location Picker Modal */}
<Modal
visible={showLocationPicker}
animationType="slide"
transparent={true}
onRequestClose={() => setShowLocationPicker(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Select Location</Text>
<TouchableOpacity
style={styles.modalCloseButton}
onPress={() => setShowLocationPicker(false)}
>
<Ionicons name="close" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
</View>
<FlatList
data={ROOM_LOCATIONS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={[
styles.locationOption,
location === item.id && styles.locationOptionSelected
]}
onPress={() => {
setLocation(item.id);
setShowLocationPicker(false);
}}
>
<Text style={styles.locationOptionIcon}>{item.icon}</Text>
<Text style={[
styles.locationOptionText,
location === item.id && styles.locationOptionTextSelected
]}>
{item.label}
</Text>
{location === item.id && (
<Ionicons name="checkmark" size={20} color={AppColors.primary} />
)}
</TouchableOpacity>
)}
ItemSeparatorComponent={() => <View style={styles.locationSeparator} />}
/>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
padding: Spacing.xs,
},
headerTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
placeholder: {
width: 32,
},
simulatorWarning: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.warningLight,
paddingVertical: Spacing.xs,
paddingHorizontal: Spacing.md,
gap: Spacing.xs,
borderBottomWidth: 1,
borderBottomColor: AppColors.warning,
},
simulatorWarningText: {
fontSize: FontSizes.xs,
color: AppColors.warning,
fontWeight: FontWeights.medium,
flex: 1,
},
content: {
flex: 1,
},
scrollContent: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: Spacing.md,
},
loadingText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
// Sensor Card
sensorCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
marginBottom: Spacing.lg,
...Shadows.sm,
},
sensorIcon: {
width: 80,
height: 80,
borderRadius: BorderRadius.lg,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
marginRight: Spacing.md,
},
sensorInfo: {
flex: 1,
},
sensorName: {
fontSize: FontSizes.xl,
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
},
statusText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
fontWeight: FontWeights.medium,
},
statusSeparator: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
lastSeenText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
// Section
section: {
marginBottom: Spacing.lg,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.textSecondary,
marginBottom: Spacing.md,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// Details Card
detailsCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
...Shadows.xs,
},
detailRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: Spacing.sm,
},
detailLabel: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
},
detailValue: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
detailDivider: {
height: 1,
backgroundColor: AppColors.border,
},
// Editable fields
editableRow: {
paddingVertical: Spacing.sm,
},
editableLabel: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginBottom: Spacing.xs,
},
editableInput: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
backgroundColor: AppColors.background,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
borderWidth: 1,
borderColor: AppColors.border,
},
editableInputMultiline: {
minHeight: 60,
textAlignVertical: 'top',
},
// Picker Button
pickerButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: AppColors.background,
borderRadius: BorderRadius.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
borderWidth: 1,
borderColor: AppColors.border,
minHeight: 44,
},
pickerButtonText: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
},
pickerButtonPlaceholder: {
color: AppColors.textMuted,
},
saveButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
borderRadius: BorderRadius.md,
marginTop: Spacing.sm,
gap: Spacing.xs,
},
saveButtonText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
// BLE Connection
connectedCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
...Shadows.xs,
},
connectedHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
marginBottom: Spacing.md,
},
connectedText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.success,
},
disconnectButton: {
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
borderRadius: BorderRadius.md,
borderWidth: 1,
borderColor: AppColors.border,
alignItems: 'center',
},
disconnectButtonText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.textSecondary,
},
connectButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
gap: Spacing.sm,
...Shadows.sm,
},
connectButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
// WiFi Status
loadingWiFiCard: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
...Shadows.xs,
},
loadingWiFiText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
wifiCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
...Shadows.xs,
},
wifiHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
marginBottom: Spacing.md,
},
wifiInfo: {
flex: 1,
},
wifiSSID: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
marginBottom: 2,
},
wifiSignal: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
},
changeWiFiButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
borderRadius: BorderRadius.md,
borderWidth: 1,
borderColor: AppColors.primary,
},
changeWiFiText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.primary,
},
noWiFiCard: {
alignItems: 'center',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.xl,
...Shadows.xs,
},
noWiFiText: {
fontSize: FontSizes.base,
color: AppColors.textMuted,
marginTop: Spacing.md,
marginBottom: Spacing.md,
},
setupWiFiButton: {
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.lg,
},
setupWiFiText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.primary,
},
// Actions
actionsCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
...Shadows.xs,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
paddingVertical: Spacing.sm,
},
actionButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.medium,
color: AppColors.primary,
},
actionDivider: {
height: 1,
backgroundColor: AppColors.border,
marginVertical: Spacing.sm,
},
// Info Card
infoCard: {
backgroundColor: AppColors.infoLight,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
},
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
marginBottom: Spacing.xs,
},
infoTitle: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.info,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.info,
lineHeight: 20,
},
// Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: AppColors.surface,
borderTopLeftRadius: BorderRadius.xl,
borderTopRightRadius: BorderRadius.xl,
maxHeight: '70%',
paddingBottom: Spacing.xl,
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
modalTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
modalCloseButton: {
padding: Spacing.xs,
},
locationOption: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
gap: Spacing.md,
},
locationOptionSelected: {
backgroundColor: AppColors.primaryLighter,
},
locationOptionIcon: {
fontSize: 24,
width: 32,
textAlign: 'center',
},
locationOptionText: {
flex: 1,
fontSize: FontSizes.base,
color: AppColors.textPrimary,
},
locationOptionTextSelected: {
fontWeight: FontWeights.semibold,
color: AppColors.primary,
},
locationSeparator: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 32 + Spacing.md,
},
});