514 lines
17 KiB
TypeScript
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',
|
|
},
|
|
}); |