Skip to main content

Overview

The vehicle management system allows users to register and manage multiple vehicles for use in parking reservations. Each vehicle includes details like brand, model, color, and license plate.

Vehicle Registration

Required Fields

License Plate

Required - Unique identifier for the vehicle

Brand

Required - Vehicle manufacturer (e.g., Toyota, Honda)

Model

Optional - Specific model name (e.g., Corolla, Civic)

Color

Optional - Vehicle color for easy identification

Alias

Optional - Custom nickname (e.g., “Auto de Papá”)

Adding a Vehicle

const handleAddVehicle = async () => {
  // Validation
  if (!newPlate.trim() || !newBrand.trim()) {
    Alert.alert(
      "Falta información", 
      "La Placa y la Marca son obligatorias."
    );
    return;
  }

  setSaving(true);
  
  try {
    await addDoc(collection(db, "vehicles"), {
      userId: auth.currentUser?.uid,
      alias: newAlias || `${newBrand} ${newModel}`,
      plate: newPlate.toUpperCase().replace(/\s/g, ''),
      brand: newBrand.trim(),
      model: newModel.trim(),
      color: newColor.trim(),
      createdAt: new Date()
    });

    setModalVisible(false);
    fetchVehicles();
    Alert.alert("¡Listo!", "Vehículo agregado correctamente.");
  } catch (error) {
    Alert.alert("Error", "No se pudo guardar el vehículo.");
  } finally {
    setSaving(false);
  }
};
License plates are automatically converted to uppercase and spaces are removed for consistency.

Vehicle Form

The add vehicle form provides a clean interface:
<Modal visible={modalVisible} animationType="slide" transparent={true}>
  <KeyboardAvoidingView 
    behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 
    style={styles.modalOverlay}
  >
    <View style={styles.modalContent}>
      <Text style={styles.modalTitle}>Agregar Auto</Text>
      
      <ScrollView showsVerticalScrollIndicator={false}>
        {/* Alias */}
        <Text style={styles.label}>Apodo (Opcional)</Text>
        <TextInput 
          style={styles.input} 
          placeholder="Ej. El Rojo, Auto de Papá..." 
          value={newAlias}
          onChangeText={setNewAlias}
        />

        {/* Brand and Model */}
        <View style={styles.row}>
          <View style={{ flex: 1, marginRight: 10 }}>
            <Text style={styles.label}>Marca *</Text>
            <TextInput 
              style={styles.input} 
              placeholder="Toyota" 
              value={newBrand}
              onChangeText={setNewBrand}
            />
          </View>
          <View style={{ flex: 1 }}>
            <Text style={styles.label}>Modelo</Text>
            <TextInput 
              style={styles.input} 
              placeholder="Corolla" 
              value={newModel}
              onChangeText={setNewModel}
            />
          </View>
        </View>

        {/* Color and Plate */}
        <View style={styles.row}>
          <View style={{ flex: 1, marginRight: 10 }}>
            <Text style={styles.label}>Color</Text>
            <TextInput 
              style={styles.input} 
              placeholder="Gris" 
              value={newColor}
              onChangeText={setNewColor}
            />
          </View>
          <View style={{ flex: 1 }}>
            <Text style={styles.label}>Placa / Matrícula *</Text>
            <TextInput 
              style={[styles.input, styles.plateInput]} 
              placeholder="ABC-123" 
              value={newPlate}
              onChangeText={setNewPlate}
              autoCapitalize="characters"
            />
          </View>
        </View>
      </ScrollView>

      {/* Action Buttons */}
      <View style={styles.modalButtons}>
        <TouchableOpacity 
          style={styles.cancelButton} 
          onPress={() => setModalVisible(false)}
        >
          <Text style={styles.cancelText}>Cancelar</Text>
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={styles.saveButton} 
          onPress={handleAddVehicle} 
          disabled={saving}
        >
          {saving ? (
            <ActivityIndicator color="#000" />
          ) : (
            <Text style={styles.saveText}>Guardar Auto</Text>
          )}
        </TouchableOpacity>
      </View>
    </View>
  </KeyboardAvoidingView>
</Modal>

Vehicle Display

Vehicles are displayed in a card-based list:
const renderItem = ({ item }: any) => (
  <View style={styles.card}>
    <View style={styles.cardIcon}>
      <Ionicons name="car-sport" size={32} color="#000" />
    </View>
    
    <View style={styles.cardInfo}>
      <Text style={styles.carAlias}>{item.alias}</Text>
      
      <Text style={styles.carDetails}>
        {item.brand} {item.model} {item.color ? `• ${item.color}` : ''}
      </Text>

      <View style={styles.plateContainer}>
        <Text style={styles.carPlate}>{item.plate}</Text>
      </View>
    </View>

    <TouchableOpacity 
      onPress={() => handleDelete(item.id)} 
      style={styles.deleteBtn}
    >
      <Ionicons name="trash-outline" size={22} color="#D93025" />
    </TouchableOpacity>
  </View>
);

Card Design Features

Yellow circular badge with car icon for visual consistency:
cardIcon: { 
  width: 55, height: 55, 
  borderRadius: 27.5, 
  backgroundColor: '#FFE100', 
  justifyContent: 'center', 
  alignItems: 'center', 
  borderWidth: 2, 
  borderColor: '#000', 
  borderStyle: 'dashed'
}
Styled container mimicking real license plates:
plateContainer: { 
  backgroundColor: '#F0F0F0', 
  alignSelf: 'flex-start', 
  paddingHorizontal: 10, 
  paddingVertical: 4, 
  borderRadius: 6, 
  borderWidth: 1, 
  borderColor: '#DDD' 
},
carPlate: { 
  fontSize: 12, 
  fontWeight: '900', 
  color: '#333', 
  letterSpacing: 1 
}

Loading Vehicles

Vehicles are fetched from Firebase on mount:
const fetchVehicles = async () => {
  if (!auth.currentUser) return;
  
  setLoading(true);
  
  try {
    const q = query(
      collection(db, "vehicles"), 
      where("userId", "==", auth.currentUser.uid)
    );
    
    const querySnapshot = await getDocs(q);
    const cars: any[] = [];
    
    querySnapshot.forEach((doc) => {
      cars.push({ id: doc.id, ...doc.data() });
    });
    
    setVehicles(cars);
  } catch (error) {
    Alert.alert("Error", "No se pudieron cargar tus vehículos.");
  } finally {
    setLoading(false);
  }
};

useEffect(() => {
  fetchVehicles();
}, []);

Deleting Vehicles

Users can remove vehicles with confirmation:
const handleDelete = (id: string) => {
  Alert.alert(
    "Eliminar Vehículo",
    "¿Estás seguro de eliminar este auto?",
    [
      { text: "Cancelar", style: "cancel" },
      { 
        text: "Eliminar", 
        style: "destructive", 
        onPress: async () => {
          await deleteDoc(doc(db, "vehicles", id));
          fetchVehicles();
        }
      }
    ]
  );
};
Deleting a vehicle is permanent and cannot be undone. Ensure the vehicle is not associated with active reservations.

Empty State

When no vehicles are registered:
<ListEmptyComponent>
  <View style={styles.emptyContainer}>
    <Ionicons name="car-outline" size={80} color="#DDD" />
    <Text style={styles.emptyText}>Tu garaje está vacío</Text>
    <Text style={styles.emptySubText}>
      Agrega tu primer auto para comenzar.
    </Text>
  </View>
</ListEmptyComponent>

Floating Action Button

A prominent FAB provides quick access to add vehicles:
<TouchableOpacity 
  style={styles.fab} 
  onPress={() => setModalVisible(true)}
>
  <Ionicons name="add" size={32} color="#000" />
</TouchableOpacity>

FAB Styling

fab: {
  position: 'absolute',
  bottom: 30,
  right: 30,
  width: 65,
  height: 65,
  borderRadius: 32.5,
  backgroundColor: '#FFE100',
  justifyContent: 'center',
  alignItems: 'center',
  shadowColor: "#000",
  shadowOffset: { width: 0, height: 4 },
  shadowOpacity: 0.3,
  elevation: 8,
  borderWidth: 2,
  borderColor: '#000'
}
The FAB remains visible while scrolling, ensuring users can always add a vehicle without scrolling back to the top.

Vehicle Selection

During reservation creation, users select from their vehicles:
<TouchableOpacity 
  style={styles.dropdown} 
  onPress={() => setShowVehiclePicker(true)}
>
  <View>
    <Text style={styles.inputLabelSmall}>Vehículo</Text>
    <Text style={styles.inputValue}>
      {selectedVehicle 
        ? `${selectedVehicle.alias}${selectedVehicle.plate}` 
        : "Seleccionar..."}
    </Text>
  </View>
  <Ionicons name="car-outline" size={24} color="#666" />
</TouchableOpacity>

Data Structure

interface Vehicle {
  id: string;           // Firestore document ID
  userId: string;       // Owner's Firebase UID
  alias: string;        // Display name
  plate: string;        // License plate (uppercase, no spaces)
  brand: string;        // Manufacturer
  model: string;        // Model name
  color: string;        // Vehicle color
  createdAt: Date;      // Registration timestamp
}

Best Practices

Always normalize license plates:
  • Convert to uppercase
  • Remove all spaces
  • Validate format if needed
If user doesn’t provide an alias, generate one from brand and model:
alias: newAlias || `${newBrand} ${newModel}`
Always filter vehicles by userId to prevent data leaks:
where("userId", "==", auth.currentUser.uid)

Build docs developers (and LLMs) love