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>
This commit is contained in:
Sergei 2026-01-24 14:21:04 -08:00
parent 2aff43af34
commit 3bc0d2a8a9

View File

@ -8,13 +8,15 @@ import {
Alert, Alert,
ActivityIndicator, ActivityIndicator,
TextInput, TextInput,
Modal,
FlatList,
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router'; import { router, useLocalSearchParams } from 'expo-router';
import * as Device from 'expo-device'; import * as Device from 'expo-device';
import { useBLE } from '@/contexts/BLEContext'; import { useBLE } from '@/contexts/BLEContext';
import { api } from '@/services/api'; import { api, ROOM_LOCATIONS, type RoomLocationId } from '@/services/api';
import type { WiFiStatus } from '@/services/ble'; import type { WiFiStatus } from '@/services/ble';
import { import {
AppColors, AppColors,
@ -58,8 +60,9 @@ export default function DeviceSettingsScreen() {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
// Editable fields // Editable fields
const [location, setLocation] = useState(''); const [location, setLocation] = useState<RoomLocationId | ''>('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [showLocationPicker, setShowLocationPicker] = useState(false);
const isConnected = connectedDevices.has(deviceId!); const isConnected = connectedDevices.has(deviceId!);
@ -82,13 +85,12 @@ export default function DeviceSettingsScreen() {
if (sensor) { if (sensor) {
setSensorInfo(sensor); setSensorInfo(sensor);
setLocation(sensor.location || ''); setLocation((sensor.location as RoomLocationId) || '');
setDescription(sensor.description || ''); setDescription(sensor.description || '');
} else { } else {
throw new Error('Sensor not found'); throw new Error('Sensor not found');
} }
} catch (error: any) { } catch (error: any) {
console.error('[DeviceSettings] Failed to load sensor info:', error);
Alert.alert('Error', 'Failed to load sensor information'); Alert.alert('Error', 'Failed to load sensor information');
router.back(); router.back();
} finally { } finally {
@ -111,7 +113,6 @@ export default function DeviceSettingsScreen() {
// Load WiFi status after connecting // Load WiFi status after connecting
loadWiFiStatus(); loadWiFiStatus();
} catch (error: any) { } catch (error: any) {
console.error('[DeviceSettings] Connection failed:', error);
Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.'); Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.');
} finally { } finally {
setIsConnecting(false); setIsConnecting(false);
@ -127,7 +128,6 @@ export default function DeviceSettingsScreen() {
const wifiStatus = await getCurrentWiFi(deviceId!); const wifiStatus = await getCurrentWiFi(deviceId!);
setCurrentWiFi(wifiStatus); setCurrentWiFi(wifiStatus);
} catch (error: any) { } catch (error: any) {
console.error('[DeviceSettings] Failed to get WiFi status:', error);
Alert.alert('Error', 'Failed to get WiFi status'); Alert.alert('Error', 'Failed to get WiFi status');
} finally { } finally {
setIsLoadingWiFi(false); setIsLoadingWiFi(false);
@ -173,9 +173,8 @@ export default function DeviceSettingsScreen() {
Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.'); Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.');
router.back(); router.back();
} catch (error: any) { } catch (error: any) {
console.error('[DeviceSettings] Reboot failed:', error);
Alert.alert('Error', 'Failed to reboot sensor'); Alert.alert('Error', 'Failed to reboot sensor');
} finally { } finally{
setIsRebooting(false); setIsRebooting(false);
} }
}, },
@ -218,7 +217,6 @@ export default function DeviceSettingsScreen() {
Alert.alert('Success', 'Device information updated.'); Alert.alert('Success', 'Device information updated.');
} catch (error: any) { } catch (error: any) {
console.error('[DeviceSettings] Save failed:', error);
Alert.alert('Error', error.message || 'Failed to save device information'); Alert.alert('Error', error.message || 'Failed to save device information');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
@ -353,13 +351,20 @@ export default function DeviceSettingsScreen() {
<View style={styles.detailsCard}> <View style={styles.detailsCard}>
<View style={styles.editableRow}> <View style={styles.editableRow}>
<Text style={styles.editableLabel}>Location</Text> <Text style={styles.editableLabel}>Location</Text>
<TextInput <TouchableOpacity
style={styles.editableInput} style={styles.pickerButton}
value={location} onPress={() => setShowLocationPicker(true)}
onChangeText={setLocation} >
placeholder="e.g., Living Room, Kitchen..." <Text style={[
placeholderTextColor={AppColors.textMuted} 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>
<View style={styles.detailDivider} /> <View style={styles.detailDivider} />
<View style={styles.editableRow}> <View style={styles.editableRow}>
@ -514,6 +519,56 @@ export default function DeviceSettingsScreen() {
</Text> </Text>
</View> </View>
</ScrollView> </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> </SafeAreaView>
); );
} }
@ -688,6 +743,26 @@ const styles = StyleSheet.create({
minHeight: 60, minHeight: 60,
textAlignVertical: 'top', 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: { saveButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -872,4 +947,63 @@ const styles = StyleSheet.create({
color: AppColors.info, color: AppColors.info,
lineHeight: 20, 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,
},
}); });