DRP_Sajidaat/DRP-App/app/add_mosque.tsx

514 lines
17 KiB
TypeScript

// app/add_mosque.tsx
import React, { useState, useEffect, useRef } from 'react'; // useRef added for debounceTimeout
import {
View,
Text,
TextInput,
Button,
StyleSheet,
ScrollView,
Alert,
ActivityIndicator,
TouchableOpacity,
Platform, // Ensure Platform is imported for style usage
Switch,
FlatList, // Added FlatList for autocomplete suggestions
} from 'react-native';
import * as Location from 'expo-location';
import { Picker } from '@react-native-picker/picker';
import { useRouter, useLocalSearchParams } from 'expo-router';
interface AddPlaceResponse {
message: string;
placeId?: string;
}
const BACKEND_URL = 'http://132.145.65.145:8080'; // Your backend URL
const GOOGLE_API_KEY = 'AIzaSyB1WZHDqjGk696AmVw7tA2sMAuOurt552Q';
export default function AddMosqueScreen() {
const router = useRouter();
const params = useLocalSearchParams();
const lat = params.lat;
const lng = params.lng;
const addressParam = params.address;
const nameParam = params.name;
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const [latitude, setLatitude] = useState<string>('');
const [longitude, setLongitude] = useState<string>('');
const [locationType, setLocationType] = useState<'mosque' | 'other' | 'outdoor_space' | 'multi_faith_room'>('mosque');
const [notes, setNotes] = useState('');
const [website, setWebsite] = useState(''); // NEW STATE FOR WEBSITE
const [isLoadingLocation, setIsLoadingLocation] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isGeocoding, setIsGeocoding] = useState(false); // Used for Google Details API fetch state
const [coordsObtained, setCoordsObtained] = useState(false);
// Autocomplete specific states
const [addressSuggestions, setAddressSuggestions] = useState<any[]>([]); // Google predictions have { description, place_id }
const [isAddressLoading, setIsAddressLoading] = useState(false); // Indicates loading for autocomplete suggestions
const debounceTimeout = useRef<NodeJS.Timeout | null>(null); // For debouncing input
// Track if user has interacted with the switches
const [womensSpace, setWomensSpace] = useState<null | boolean>(null);
const [wudu, setWudu] = useState<null | boolean>(null);
// Effect to auto-populate fields from query params on initial load
useEffect(() => {
if (lat && typeof lat === 'string') {
setLatitude(lat);
}
if (lng && typeof lng === 'string') {
setLongitude(lng);
}
if (addressParam && typeof addressParam === 'string') {
setAddress(decodeURIComponent(addressParam));
}
if (nameParam && typeof nameParam === 'string') {
setName(decodeURIComponent(nameParam));
}
if (lat && lng) {
setCoordsObtained(true);
}
}, [lat, lng, addressParam, nameParam]);
const getCurrentLocation = async () => {
setIsLoadingLocation(true);
setCoordsObtained(false); // Reset status
setLatitude(''); // Clear old coords
setLongitude(''); // Clear old coords
setAddress(''); // Clear address for new reverse geocoding
setAddressSuggestions([]); // Clear suggestions
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission denied', 'Permission to access location was denied. Cannot auto-fill coordinates.');
setIsLoadingLocation(false);
return;
}
try {
let location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
setLatitude(location.coords.latitude.toString());
setLongitude(location.coords.longitude.toString());
setCoordsObtained(true);
const reverseGeocode = await Location.reverseGeocodeAsync({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
});
if (reverseGeocode && reverseGeocode.length > 0) {
const firstResult = reverseGeocode[0];
const formattedAddress = [
firstResult.name,
firstResult.street,
firstResult.city,
firstResult.postalCode,
firstResult.country,
].filter(Boolean).join(', ');
setAddress(formattedAddress);
}
} catch (error) {
console.error("Error getting current location:", error);
Alert.alert('Error', 'Failed to get current location. Please enter manually.');
} finally {
setIsLoadingLocation(false);
}
};
const fetchGoogleSuggestions = async (input: string) => {
if (!input || input.length < 3) {
setAddressSuggestions([]);
return;
}
// Debounce is now handled by handleAddressChangeWithDebounce,
// so this function itself doesn't need to debounce.
setIsAddressLoading(true); // Indicate loading for suggestions
try {
const url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(input)}&key=${GOOGLE_API_KEY}&components=country:gb&location=51.5074,-0.1278&radius=20000`;
const response = await fetch(url);
const data = await response.json();
if (data && data.predictions) {
setAddressSuggestions(data.predictions);
} else {
setAddressSuggestions([]);
}
} catch (e) {
console.error('Google Autocomplete fetch error:', e);
setAddressSuggestions([]);
} finally {
setIsAddressLoading(false); // Stop loading for suggestions
}
};
const fetchPlaceDetails = async (placeId: string, description: string) => {
setIsGeocoding(true); // Indicates loading for place details (lat/lng)
setCoordsObtained(false); // Reset status while fetching details
setLatitude('');
setLongitude('');
setAddressSuggestions([]); // Hide suggestions list
try {
const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=geometry&key=${GOOGLE_API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data && data.result && data.result.geometry) {
const { lat, lng } = data.result.geometry.location;
setAddress(description);
setLatitude(lat.toString());
setLongitude(lng.toString());
setCoordsObtained(true);
Alert.alert('Location Found', `Coordinates for "${description}" obtained.`);
} else {
Alert.alert('Error', 'Could not retrieve coordinates for the selected place.');
}
} catch (e) {
console.error('Google Place Details fetch error:', e);
Alert.alert('Error', 'Failed to get location details. Please try again.');
} finally {
setIsGeocoding(false); // Stop loading for place details
}
};
// THIS IS THE DEBOUNCED HANDLER FOR THE ADDRESS TEXTINPUT
const handleAddressChangeWithDebounce = (text: string) => {
setAddress(text); // Update address state immediately
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
debounceTimeout.current = setTimeout(() => {
fetchGoogleSuggestions(text); // Call Google Autocomplete after debounce
}, 500); // Debounce for 500ms
};
const handleSuggestionPress = (item: any) => { // 'item' is a Google prediction object
fetchPlaceDetails(item.place_id, item.description);
};
const handleSubmit = async () => {
// Ensure essential fields are filled and coordinates are obtained
if (!name || !address || !latitude || !longitude || !locationType || womensSpace === null || wudu === null || !coordsObtained) {
Alert.alert('Missing Info', 'Please fill in all required fields (Name, Address, Location Type, Women\'s Space, Wudu Facilities) and ensure coordinates are obtained (use current location or select from address suggestions).');
return;
}
setIsSubmitting(true);
const payload = {
name,
address,
latitude: parseFloat(latitude),
longitude: parseFloat(longitude),
location_type: locationType,
womens_space: womensSpace,
wudu: wudu,
notes: notes,
website_url: website, // Include website in payload
};
try {
console.log("Sending new place payload:", payload);
const response = await fetch(`${BACKEND_URL}/places/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
console.error("Backend error response:", errorData);
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
}
const result: AddPlaceResponse = await response.json();
Alert.alert('Success', result.message || 'Prayer Space added successfully!');
setName('');
setAddress('');
setLatitude('');
setLongitude('');
setLocationType('mosque');
setWomensSpace(null);
setWudu(null);
setNotes('');
setWebsite(''); // Clear website field
setCoordsObtained(false);
router.back();
} catch (error: any) {
console.error('Add mosque error:', error);
Alert.alert('Error', `Failed to add prayer space: ${error.message || 'Network error'}`);
} finally {
setIsSubmitting(false);
}
};
// Styles for Yes/No buttons for switches
const getSwitchButtonStyle = (selected: boolean | null, isYes: boolean) => ({
marginRight: 10,
borderWidth: 1,
borderColor: selected === isYes ? '#007bff' : '#ccc',
backgroundColor: selected === isYes ? '#e6f0ff' : '#fff',
borderRadius: 6,
paddingHorizontal: 16,
paddingVertical: 8,
marginLeft: 0,
});
const getSwitchButtonTextStyle = (selected: boolean | null, isYes: boolean) => ({
color: selected === isYes ? '#007bff' : '#333',
fontWeight: selected === isYes ? 'bold' : 'normal',
});
return (
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
<Text style={styles.title}>Add New Prayer Space</Text>
<Text style={styles.label}>Location Name *</Text>
<TextInput
style={styles.input}
placeholder="e.g., London Central Mosque"
value={name}
onChangeText={setName}
/>
<Text style={styles.label}>Address *</Text>
<TextInput
style={styles.input}
placeholder="Start typing an address or use current location"
value={address}
onChangeText={handleAddressChangeWithDebounce} // Correctly using debounced handler
onFocus={() => { if (address.length >=3 && addressSuggestions.length > 0) setAddressSuggestions(addressSuggestions); else if (address.length >=3) fetchGoogleSuggestions(address); }}
onBlur={() => setTimeout(() => setAddressSuggestions([]), 200)}
autoCorrect={false}
autoCapitalize="none"
/>
{isAddressLoading && <ActivityIndicator size="small" color="#007bff" style={{ marginBottom: 10 }} />}
{addressSuggestions.length > 0 && (
<ScrollView style={styles.suggestionsContainer} nestedScrollEnabled={true}>
{addressSuggestions.map((item) => (
<TouchableOpacity
key={item.place_id}
style={styles.suggestionItem}
onPress={() => fetchPlaceDetails(item.place_id, item.description)}
>
<Text>{item.description}</Text>
</TouchableOpacity>
))}
</ScrollView>
)}
<View style={styles.buttonRow}>
<Button
title={isLoadingLocation ? "Getting Location..." : "Use Current Location"}
onPress={getCurrentLocation}
disabled={isLoadingLocation || isGeocoding}
/>
</View>
{/* Display status of coordinates */}
{coordsObtained && latitude && longitude && (
<Text style={styles.coordsStatusText}>
Coordinates obtained: Lat {parseFloat(latitude).toFixed(4)}, Lng {parseFloat(longitude).toFixed(4)}
</Text>
)}
<Text style={styles.label}>Prayer Space Type *</Text>
<View style={styles.pickerContainer}>
<Picker
selectedValue={locationType}
// UPDATED: locationType values
onValueChange={(itemValue: 'mosque' | 'other' | 'outdoor_space' | 'multi_faith_room') => setLocationType(itemValue)}
style={styles.picker}
>
<Picker.Item label="Mosque" value="mosque" />
<Picker.Item label="Outdoor Space" value="outdoor_space" />
<Picker.Item label="Multi-Faith Room" value="multi_faith_room" />
<Picker.Item label="Other Prayer Space" value="other" />
</Picker>
</View>
<View style={styles.switchRow}>
<Text style={styles.label}>Women's Space Available *</Text>
<View style={{ flexDirection: 'row' }}>
<TouchableOpacity
style={getSwitchButtonStyle(womensSpace, false)}
onPress={() => setWomensSpace(false)}
>
<Text style={getSwitchButtonTextStyle(womensSpace, false)}>No</Text>
</TouchableOpacity>
<TouchableOpacity
style={getSwitchButtonStyle(womensSpace, true)}
onPress={() => setWomensSpace(true)}
>
<Text style={getSwitchButtonTextStyle(womensSpace, true)}>Yes</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.switchRow}>
<Text style={styles.label}>Wudu Facilities Available *</Text>
<View style={{ flexDirection: 'row' }}>
<TouchableOpacity
style={getSwitchButtonStyle(wudu, false)}
onPress={() => setWudu(false)}
>
<Text style={getSwitchButtonTextStyle(wudu, false)}>No</Text>
</TouchableOpacity>
<TouchableOpacity
style={getSwitchButtonStyle(wudu, true)}
onPress={() => setWudu(true)}
>
<Text style={getSwitchButtonTextStyle(wudu, true)}>Yes</Text>
</TouchableOpacity>
</View>
</View>
<Text style={styles.label}>Website URL (Optional)</Text>
<TextInput
style={styles.input}
placeholder="e.g., https://example.com"
value={website}
onChangeText={setWebsite}
keyboardType="url"
autoCapitalize="none"
/>
<Text style={styles.label}>Notes</Text>
<TextInput
style={[styles.input, styles.notesInput]}
placeholder="Any additional notes (e.g., opening hours, specific features)"
value={notes}
onChangeText={setNotes}
multiline
numberOfLines={4}
textAlignVertical="top"
/>
<Button
title={isSubmitting ? "Adding Prayer Space..." : "Add Prayer Space"}
onPress={handleSubmit}
disabled={isSubmitting || isLoadingLocation || isGeocoding || !coordsObtained || womensSpace === null || wudu === null}
/>
<TouchableOpacity style={styles.cancelButton} onPress={() => router.back()}>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
contentContainer: {
padding: 20,
paddingBottom: 40,
},
title: {
fontSize: 26,
fontWeight: 'bold',
marginBottom: 30,
textAlign: 'center',
color: '#333',
},
label: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
input: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
paddingVertical: 12,
fontSize: 16,
marginBottom: 15,
},
notesInput: {
height: 100,
paddingTop: 12,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 15,
},
pickerContainer: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
marginBottom: 15,
overflow: 'hidden',
},
picker: {
height: Platform.OS === 'ios' ? 120 : 50,
width: '100%',
},
switchRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
paddingVertical: 12,
},
coordsStatusText: {
fontSize: 14,
color: '#007bff',
textAlign: 'center',
marginBottom: 15,
},
geocodingIndicator: {
marginTop: -5,
marginBottom: 10,
},
cancelButton: {
marginTop: 15,
paddingVertical: 10,
alignItems: 'center',
},
cancelButtonText: {
color: '#dc3545',
fontSize: 16,
fontWeight: '600',
},
suggestionsContainer: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 6,
marginBottom: 15,
maxHeight: 150,
overflow: 'hidden',
},
suggestionItem: {
paddingVertical: 10,
paddingHorizontal: 12,
borderBottomColor: '#eee',
borderBottomWidth: 1,
width: '100%',
},
suggestionText: {
fontSize: 16,
color: '#333',
},
});