938 lines
30 KiB
TypeScript

// map.tsx
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { StyleSheet, View, Text, ActivityIndicator, Platform, Linking, TouchableOpacity, Modal, Dimensions, ScrollView, Image, Alert } from 'react-native';
import MapView, { Camera, Marker, Region } from 'react-native-maps';
import { useLocalSearchParams, useFocusEffect, useRouter } from 'expo-router';
import * as Location from 'expo-location';
import { Ionicons } from '@expo/vector-icons';
import SearchComponent from '../SearchComponent'; // Import the new SearchComponent
import { getListItemStyle } from './mapList';
// Interface for prayer spaces on map (MATCHES BACKEND'S CAPITALIZED KEYS AND ALL REQUIRED FIELDS)
interface MapPrayerSpace {
ID: string;
Name: string;
Latitude: number;
Longitude: number;
Address: string;
LocationType: 'Mosque' | 'Prayer Room' | 'Community Space' | 'other';
WomensSpace: boolean;
Wudu: boolean;
OpeningHours: string;
Notes?: string;
Website?: string;
Clean: number;
CleanWudu: number;
Quiet: number;
Privateness: number;
ChildFriendly: number;
Safe: number;
NumberOfReviews: number;
Reviews: any[];
Images: any[];
}
// ImageObject and Review Interfaces (copied from PrayerSpacesListPage for consistency)
export interface ImageObject {
id?: string;
url: string;
note?: string;
}
export interface Review {
ID: string;
Rating?: number;
Quiet: number;
Clean: number;
Private: number;
Comment?: string;
User?: string;
CleanWudu?: number;
ChildFriendly?: number;
Safe?: number;
}
const KAABA_COORDS = {
latitude: 21.4225,
longitude: 39.8262,
};
const BACKEND_URL = 'http://132.145.65.145:8080'; // Your backend URL
const defaultRegion: Region = {
latitude: 51.5074, // London
longitude: -0.1278,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
};
const TARGET_LATITUDE_DELTA = 0.005; // Zoom level for specific locations
const TARGET_LONGITUDE_DELTA = 0.005;
const USER_LATITUDE_DELTA = 0.02; // Zoom level for user's location
const USER_LONGITUDE_DELTA = 0.02;
// Image URL Conversion (copied from PrayerSpacesListPage)
function convertToFullImageUrl(imageUrl: string): string {
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
return `http://132.145.65.145:8080${imageUrl}`;
}
// Emoji Rating Helper (copied from PrayerSpacesListPage)
function averageRatingToEmoji(avg: number) {
if (avg < 1.66) {
return "😞";
} else if (avg < 2.33) {
return "😐";
} else {
return "😊";
}
}
// Prayer Space Type Map (copied from PrayerSpacesListPage)
function prayerSpaceTypeMap(str: string) {
switch (str) {
case "mosque":
return "Mosque"
default:
return "Other"
}
}
export default function MapScreen() {
const params = useLocalSearchParams<{ latitude?: string; longitude?: string; name?: string }>();
const router = useRouter();
// mapRegionToSet now primarily drives the initial position via initialRegion
const [mapRegionToSet, setMapRegionToSet] = useState<Region | null>(null);
const [userCoords, setUserCoords] = useState<Location.LocationObjectCoords | null>(null);
const [permissionStatus, setPermissionStatus] = useState<Location.PermissionStatus | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [mapBearing, setMapBearing] = useState<number>(0);
const [prayerSpaces, setPrayerSpaces] = useState<MapPrayerSpace[]>([]);
const updateBearingTimeoutRef = useRef<number | null>(null);
const [isInfoModalVisible, setIsInfoModalVisible] = useState(false);
const [selectedMosqueInfo, setSelectedMosqueInfo] = useState<MapPrayerSpace | null>(null);
const mapRef = useRef<MapView>(null);
const hasAppliedInitialRegionLogic = useRef(false);
const fetchPrayerSpaces = useCallback(async () => {
try {
const response = await fetch(`${BACKEND_URL}/places/query`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const rawData: any[] = await response.json();
const processedData: MapPrayerSpace[] = rawData.map(item => ({
ID: item.ID,
Name: item.Name,
Latitude: item.Latitude,
Longitude: item.Longitude,
Address: item.Address,
LocationType: prayerSpaceTypeMap(item.LocationType),
WomensSpace: item.WomensSpace,
Wudu: item.Wudu,
OpeningHours: item.OpeningHours || '',
Notes: item.Notes,
Website: item.WebsiteURL,
Reviews: (item.Reviews || []).map((r: any) => ({
ID: r.ID,
Rating: (r.Quiet + r.Clean + r.Private) / 3.0,
Quiet: r.Quiet, Clean: r.Clean, Private: r.Private,
CleanWudu: r.CleanWudu, ChildFriendly: r.ChildFriendly, Safe: r.Safe,
Comment: r.Comment, User: r.User,
})),
Images: (item.Images || []).map((img: any) => ({
id: img.ID,
url: convertToFullImageUrl(img.ImageURL),
note: img.Notes,
})),
Clean: item.Reviews && item.Reviews.length > 0 ? item.Reviews.reduce((sum: number, r: any) => sum + (r.Clean || 0), 0) / item.Reviews.length : 0,
CleanWudu: item.Reviews && item.Reviews.length > 0 ? item.Reviews.reduce((sum: number, r: any) => sum + (r.CleanWudu || 0), 0) / item.Reviews.length : 0,
Quiet: item.Reviews && item.Reviews.length > 0 ? item.Reviews.reduce((sum: number, r: any) => sum + (r.Quiet || 0), 0) / item.Reviews.length : 0,
Privateness: item.Reviews && item.Reviews.length > 0 ? item.Reviews.reduce((sum: number, r: any) => sum + (r.Private || 0), 0) / item.Reviews.length : 0,
ChildFriendly: item.Reviews && item.Reviews.length > 0 ? item.Reviews.reduce((sum: number, r: any) => sum + (r.ChildFriendly || 0), 0) / item.Reviews.length : 0,
Safe: item.Reviews && item.Reviews.length > 0 ? item.Reviews.reduce((sum: number, r: any) => sum + (r.Safe || 0), 0) / item.Reviews.length : 0,
NumberOfReviews: item.Reviews ? item.Reviews.length : 0,
}));
console.log('[DEBUG] Fetched & processed prayer spaces for map:', processedData);
setPrayerSpaces(processedData);
} catch (error) {
console.error('Failed to fetch prayer spaces for map:', error);
}
}, []);
useFocusEffect(
useCallback(() => {
fetchPrayerSpaces();
return () => {
// Optional cleanup
};
}, [fetchPrayerSpaces])
);
const getAdjustedQiblaDirection = (): number => {
return (0 - mapBearing + 360) % 360;
};
const PrayerSpaceMarker = () => (
<View style={styles.prayerMarker}>
<Ionicons name="location-sharp" size={30} color="#007bff" />
</View>
);
// 1. Effect for Location Permission and Fetching User Coords (runs once)
useEffect(() => {
let isMounted = true;
const requestLocation = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (isMounted) setPermissionStatus(status);
if (status === 'granted') {
try {
const location = await Location.getLastKnownPositionAsync();
if (isMounted && location) {
setMapRegionToSet({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
latitudeDelta: USER_LATITUDE_DELTA,
longitudeDelta: USER_LONGITUDE_DELTA,
});
setUserCoords(location.coords);
}
} catch (error) {
console.error("Error fetching user location:", error);
}
}
};
requestLocation();
return () => {
isMounted = false;
};
}, []);
// Handler for when a place is selected from SearchComponent
// This now *only* calls animateToRegion, not setMapRegionToSet.
const handleSelectedPlaceFromSearch = useCallback((lat: number, lng: number, name: string) => {
const newRegion: Region = {
latitude: lat,
longitude: lng,
latitudeDelta: TARGET_LATITUDE_DELTA,
longitudeDelta: TARGET_LONGITUDE_DELTA,
};
if (mapRef.current) {
mapRef.current.animateToRegion(newRegion, 1000); // Direct animation
} else {
console.warn("Map reference not available to animate to selected place.");
}
// IMPORTANT: DO NOT set mapRegionToSet here if you want animateToRegion to be the sole driver for dynamic movement.
// The map will still correctly move via animateToRegion.
}, []);
// 2. Main effect to determine the map region based on params or user location
// This useFocusEffect is primarily for setting the *initial* region for `initialRegion` prop.
useFocusEffect(
useCallback(() => {
let isMounted = true;
setIsLoading(true);
hasAppliedInitialRegionLogic.current = false;
let newRegion: Region | null = null;
if (params.latitude && params.longitude) {
const targetLat = parseFloat(params.latitude);
const targetLng = parseFloat(params.longitude);
if (!isNaN(targetLat) && !isNaN(targetLng)) {
newRegion = {
latitude: targetLat,
longitude: targetLng,
latitudeDelta: TARGET_LATITUDE_DELTA,
longitudeDelta: TARGET_LONGITUDE_DELTA,
};
}
} else if (userCoords) {
newRegion = {
latitude: userCoords.latitude,
longitude: userCoords.longitude,
latitudeDelta: USER_LATITUDE_DELTA,
longitudeDelta: USER_LONGITUDE_DELTA,
};
} else if (permissionStatus && permissionStatus !== 'granted') {
newRegion = defaultRegion;
} else if (permissionStatus === 'granted' && !userCoords && !params.latitude) {
// Permission granted, but still waiting for userCoords. Keep loading.
} else if (!permissionStatus) {
// Permission status not yet determined. Keep loading.
} else {
// Any other unhandled case, fallback to default.
newRegion = defaultRegion;
}
if (newRegion && isMounted) {
setMapRegionToSet(newRegion); // Set state for initialRegion
hasAppliedInitialRegionLogic.current = true;
}
if (isMounted &&
(newRegion ||
(permissionStatus && permissionStatus !== 'granted')
|| (permissionStatus === 'granted' && !userCoords && !params.latitude)
)
)
{
if (permissionStatus === 'granted' && !userCoords && !params.latitude) {
// Still waiting for user location to be fetched by the other useEffect
} else {
setIsLoading(false);
}
}
return () => { isMounted = false; };
}, [params.latitude, params.longitude, userCoords, permissionStatus])
);
// This useEffect ensures the map animates initially, based on mapRegionToSet.
// It is NOT used for subsequent search-initiated animations.
useEffect(() => {
if (mapRegionToSet && mapRef.current && hasAppliedInitialRegionLogic.current) {
mapRef.current.animateToRegion(mapRegionToSet, 1000);
// Reset this flag so it only animates once on initial load/focus
hasAppliedInitialRegionLogic.current = false;
}
}, [mapRegionToSet]);
const openDirections = (lat: number, lng: number, placeName?: string) => {
const scheme = Platform.select({ ios: 'maps:0,0?q=', android: 'geo:0,0?q=' });
const latLng = `${lat},${lng}`;
const label = placeName || 'Destination';
const url = Platform.select({ios: `${scheme}${label}@${latLng}`, android: `${scheme}${latLng}(${label})`});
if (url) Linking.openURL(url).catch((err: any) => console.error("Failed to open maps link:", err));
};
const handleAddMosque = () => {
router.push('/add_mosque');
};
const handleMapPress = async (event: any) => {
const { coordinate } = event.nativeEvent;
console.log(coordinate);
let address = '';
try {
const geocode = await Location.reverseGeocodeAsync({
latitude: coordinate.latitude,
longitude: coordinate.longitude,
});
if (geocode && geocode.length > 0) {
const g = geocode[0];
address = [g.name, g.street, g.city, g.region, g.postalCode, g.country].filter(Boolean).join(', ');
}
} catch (err) {
console.warn('Reverse geocoding failed:', err);
}
router.push(`/add_mosque?lat=${coordinate.latitude}&lng=${coordinate.longitude}&address=${encodeURIComponent(address)}`);
};
const handlePoiClick = async (event: any) => {
const { coordinate, name } = event.nativeEvent;
let address = '';
let mainName = name;
if (typeof name === 'string') {
mainName = name.split(/\n/)[0].trim();
}
try {
const geocode = await Location.reverseGeocodeAsync({
latitude: coordinate.latitude,
longitude: coordinate.longitude,
});
if (geocode && geocode.length > 0) {
const g = geocode[0];
address = [g.name, g.street, g.city, g.region, g.postalCode, g.country].filter(Boolean).join(', ');
}
} catch (err) {
address = mainName;
}
router.push(`/add_mosque?lat=${coordinate.latitude}&lng=${coordinate.longitude}&address=${encodeURIComponent(address)}&name=${encodeURIComponent(mainName)}`);
};
// handleMarkerPress to open info modal
const handleMarkerPress = (place: MapPrayerSpace) => {
setSelectedMosqueInfo(place);
setIsInfoModalVisible(true);
};
if (isLoading && !mapRegionToSet) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007bff" />
<Text>Loading map...</Text>
</View>
);
}
// Use mapRegionToSet for initialRegion, fallback to default if null
const currentMapRegion = mapRegionToSet || defaultRegion;
const updateMapBearing = async () => {
if (mapRef.current) {
try {
const camera = await mapRef.current.getCamera();
setMapBearing(camera.heading || 0);
} catch (error) {
console.log('Error getting camera:', error);
}
}
};
const debouncedUpdateMapBearing = () => {
if (updateBearingTimeoutRef.current) {
clearTimeout(updateBearingTimeoutRef.current);
}
updateBearingTimeoutRef.current = setTimeout(() => {
updateMapBearing();
}, 20); // Update every 100ms max during interaction
};
return (
<View style={styles.container}>
{/* Search Bar is now a separate component */}
<SearchComponent onSelectPlace={handleSelectedPlaceFromSearch} />
{/* MapView: Uses initialRegion for initial position, then animateToRegion for movement */}
<MapView
ref={mapRef}
style={styles.map}
initialRegion={currentMapRegion} // Reverted to initialRegion
showsUserLocation
showsMyLocationButton
onPress={handleMapPress}
onPoiClick={handlePoiClick}
onRegionChange={() => {
debouncedUpdateMapBearing();
}}
onRegionChangeComplete={() => {
updateMapBearing();
}}>
{prayerSpaces.map((place) => (
<Marker
key={place.ID}
coordinate={{ latitude: place.Latitude, longitude: place.Longitude }}
title={place.Name}
onPress={() => handleMarkerPress(place)}
anchor={{ x: 0.5, y: 1 }}
>
<PrayerSpaceMarker />
</Marker>
))}
{/* Remove user location marker */}
</MapView>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.addMosqueButton} onPress={handleAddMosque}>
<Ionicons name="add" size={30} color="white" />
<Text style={styles.addMosqueText} numberOfLines={1}>
Add Prayer Space
</Text>
</TouchableOpacity>
</View>
{/* MosqueInfoModal */}
<MosqueInfoModal
visible={isInfoModalVisible}
onClose={() => setIsInfoModalVisible(false)}
mosque={selectedMosqueInfo}
/>
</View>
);
}
// --- MosqueInfoModal Component (Inline) ---
interface MosqueInfoModalProps {
visible: boolean;
onClose: () => void;
mosque: MapPrayerSpace | null;
}
const { width: modalWidth, height: modalHeight } = Dimensions.get('window');
const MosqueInfoModal: React.FC<MosqueInfoModalProps> = ({ visible, onClose, mosque }) => {
const [isReviewsModalVisible, setIsReviewsModalVisible] = useState(false);
const router = useRouter();
if (!mosque) {
return null;
}
const averageRatingToEmoji = (avg: number) => {
if (avg < 1.66) {
return "😞";
} else if (avg < 2.33) {
return "😐";
} else {
return "😊";
}
};
const openDirectionsInMaps = () => {
if (mosque.Latitude && mosque.Longitude) {
const scheme = Platform.select({ ios: 'maps:0,0?q=', android: 'geo:0,0?q=' });
const latLng = `${mosque.Latitude},${mosque.Longitude}`;
const label = mosque.Name || 'Destination';
const url = Platform.select({ios: `${scheme}${label}@${latLng}`, android: `${scheme}${latLng}(${label})`});
if (url) Linking.openURL(url).catch((err: any) => console.error("Failed to open maps link:", err));
}
};
const openWebsite = () => {
if (mosque.Website) {
const formattedWebsite = mosque.Website.startsWith('http') ? mosque.Website : `http://${mosque.Website}`;
Linking.openURL(formattedWebsite).catch((err: any) => console.error("Failed to open website link:", err));
}
};
const handleAddReviewFromModal = () => {
onClose();
router.push({ pathname: '/add_review', params: { placeId: mosque.ID, placeName: mosque.Name } });
};
const handleAddPhotoFromModal = () => {
onClose();
router.push({ pathname: '/upload', params: { placeId: mosque.ID, placeName: mosque.Name } });
};
const handleShowReviewsFromModal = () => {
setIsReviewsModalVisible(true);
};
return (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<View style={infoModalStyles.centeredView}>
<View style={infoModalStyles.modalView}>
<TouchableOpacity style={infoModalStyles.closeButtonTopRight} onPress={onClose}>
<Ionicons name="close-circle" size={30} color="#999" />
</TouchableOpacity>
{mosque.Images && mosque.Images.length > 0 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={infoModalStyles.thumbnailScrollView}
contentContainerStyle={infoModalStyles.thumbnailScrollViewContent}
>
{mosque.Images.map((imgObj, index) => (
<Image key={imgObj.id || `modal-img-${index}`} source={{ uri: imgObj.url }} style={infoModalStyles.thumbnailImage} />
))}
</ScrollView>
)}
<Text style={infoModalStyles.listItemName}>{mosque.Name}</Text>
{mosque.Website && (
<TouchableOpacity onPress={openWebsite}>
<Text style={[infoModalStyles.listItemText, { color: 'blue', textDecorationLine: 'underline' }]}>
{mosque.Website}
</Text>
</TouchableOpacity>
)}
<Text style={infoModalStyles.infoText}>Type: {prayerSpaceTypeMap(mosque.LocationType)}</Text>
<Text style={infoModalStyles.infoText}>Address: {mosque.Address}</Text>
{mosque.OpeningHours && <Text style={infoModalStyles.infoText}>Hours: {mosque.OpeningHours}</Text>}
{mosque.NumberOfReviews === 0 ? (
<Text style={infoModalStyles.noReviewsPlaceholderText}>No Reviews</Text>
) : (
<>
<Text style={getListItemStyle(mosque.Clean)}>
Cleanliness: {typeof mosque.Clean === 'number' ? averageRatingToEmoji(mosque.Clean) : '—'}
</Text>
<Text style={getListItemStyle(mosque.CleanWudu)}>
Wudu Cleanliness: {typeof mosque.CleanWudu === 'number' ? averageRatingToEmoji(mosque.CleanWudu) : '—'}
</Text>
<Text style={getListItemStyle(mosque.Quiet)}>
Quietness: {typeof mosque.Quiet === 'number' ? averageRatingToEmoji(mosque.Quiet) : '—'}
</Text>
<Text style={getListItemStyle(mosque.Privateness)}>
Privacy: {typeof mosque.Privateness === 'number' ? averageRatingToEmoji(mosque.Privateness) : '—'}
</Text>
<Text style={getListItemStyle(mosque.ChildFriendly)}>
Child Friendliness: {typeof mosque.ChildFriendly === 'number' ? averageRatingToEmoji(mosque.ChildFriendly) : '—'}
</Text>
<Text style={getListItemStyle(mosque.Safe)}>
Safety: {typeof mosque.Safe === 'number' ? averageRatingToEmoji(mosque.Safe) : '—'}
</Text>
</>
)}
<View style={infoModalStyles.facilitiesIconsContainer}>
{mosque.WomensSpace && <Text style={infoModalStyles.facilityIcon} accessibilityLabel="Women's Space Available">🚺 Women's Space</Text>}
{mosque.Wudu && <Text style={infoModalStyles.facilityIcon} accessibilityLabel="Wudu Available">💧 Wudu Facilities</Text>}
</View>
{mosque.Notes && <Text style={infoModalStyles.infoText}>Notes: {mosque.Notes}</Text>}
<View style={infoModalStyles.buttonRowContainer}>
<TouchableOpacity
style={[infoModalStyles.splitButtonBase, infoModalStyles.splitButtonLeft]}
onPress={handleShowReviewsFromModal}
>
<Text style={infoModalStyles.splitButtonText}>Show Reviews</Text>
</TouchableOpacity>
<View style={infoModalStyles.buttonSeparator} />
<TouchableOpacity
style={[infoModalStyles.splitButtonBase, infoModalStyles.splitButtonRight]}
onPress={openDirectionsInMaps}
>
<Text style={infoModalStyles.splitButtonText}>Get Directions</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={infoModalStyles.addReviewButton} onPress={handleAddReviewFromModal}>
<Text style={infoModalStyles.addReviewButtonText}>Add a Review</Text>
</TouchableOpacity>
<TouchableOpacity style={infoModalStyles.addPhotoButton} onPress={handleAddPhotoFromModal}>
<Text style={infoModalStyles.addPhotoButtonText}>Add a Photo</Text>
</TouchableOpacity>
{/* ReviewsModal - Now inline within MosqueInfoModal */}
<ReviewsModal
visible={isReviewsModalVisible}
onClose={() => setIsReviewsModalVisible(false)}
reviews={mosque.Reviews}
spaceName={mosque.Name}
/>
</View>
</View>
</Modal>
);
};
// --- ReviewsModal Component (Inline) ---
interface ReviewsModalProps {
visible: boolean;
onClose: () => void;
reviews: Review[];
spaceName: string;
}
const ReviewsModal: React.FC<ReviewsModalProps> = ({ visible, onClose, reviews, spaceName }) => {
return (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<View style={infoModalStyles.centeredView}>
<View style={infoModalStyles.modalView}>
<Text style={infoModalStyles.modalTitle}>Reviews for {spaceName}</Text>
<ScrollView style={infoModalStyles.infoScrollView}>
{reviews.length === 0 ? (
<Text style={infoModalStyles.noReviewsPlaceholderText}>No reviews yet.</Text>
) : (
reviews.map((review, index) => (
<View key={review.ID || `review-${index}`} style={infoModalStyles.reviewItem}>
{review.Rating !== undefined && review.Rating !== null && (
<Text style={infoModalStyles.reviewRating}>Overall Rating: {averageRatingToEmoji(review.Rating)}</Text>
)}
<Text style={infoModalStyles.reviewDetail}>Cleanliness: {typeof review.Clean === 'number' ? averageRatingToEmoji(review.Clean) : ''}</Text>
<Text style={infoModalStyles.reviewDetail}>Wudu Cleanliness: {typeof review.CleanWudu === 'number' ? averageRatingToEmoji(review.CleanWudu) : ''}</Text>
<Text style={infoModalStyles.reviewDetail}>Quietness: {typeof review.Quiet === 'number' ? averageRatingToEmoji(review.Quiet) : ''}</Text>
<Text style={infoModalStyles.reviewDetail}>Privacy: {typeof review.Private === 'number' ? averageRatingToEmoji(review.Private) : ''}</Text>
<Text style={infoModalStyles.reviewDetail}>Child Friendliness: {typeof review.ChildFriendly === 'number' ? averageRatingToEmoji(review.ChildFriendly) : ''}</Text>
<Text style={infoModalStyles.reviewDetail}>Safety: {typeof review.Safe === 'number' ? averageRatingToEmoji(review.Safe) : ''}</Text>
{review.Comment && <Text style={infoModalStyles.reviewComment}>"{review.Comment}"</Text>}
{review.User && <Text style={infoModalStyles.reviewUser}>— {review.User}</Text>}
</View>
))
)}
</ScrollView>
<TouchableOpacity style={infoModalStyles.closeButton} onPress={onClose}>
<Text style={infoModalStyles.closeButtonText}>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
};
const infoModalStyles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.6)',
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 10,
padding: 15,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
width: modalWidth * 0.9,
maxHeight: modalHeight * 0.9,
},
closeButtonTopRight: {
position: 'absolute',
top: 5,
right: 5,
zIndex: 1,
padding: 5,
},
modalTitle: {
fontSize: 22,
fontWeight: 'bold',
marginBottom: 8,
color: '#007bff',
textAlign: 'center',
},
infoScrollView: {
width: '100%',
maxHeight: 'auto',
marginBottom: 15,
},
thumbnailScrollView: {
marginBottom: 12,
maxHeight: 100,
width: '100%',
},
thumbnailScrollViewContent: {
paddingVertical: 2,
},
thumbnailImage: {
width: 100,
height: 80,
borderRadius: 6,
marginRight: 8,
backgroundColor: '#e0e0e0',
},
listItemName: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
color: '#007bff',
textAlign: 'center',
},
listItemText: {
fontSize: 14,
color: '#555',
marginBottom: 4,
lineHeight: 20,
},
infoText: {
fontSize: 14,
color: '#555',
marginBottom: 4,
lineHeight: 20,
},
facilitiesIconsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginVertical: 8,
justifyContent: 'center',
},
facilityIcon: {
fontSize: 14,
color: '#333',
backgroundColor: '#e0e0e0',
borderRadius: 5,
paddingHorizontal: 8,
paddingVertical: 4,
margin: 4,
},
noReviewsPlaceholderText: {
fontSize: 16,
color: '#777',
textAlign: 'center',
marginVertical: 8,
fontStyle: 'italic',
},
buttonRowContainer: {
flexDirection: 'row',
marginTop: 12,
borderWidth: 1,
borderColor: '#007bff',
borderRadius: 5,
overflow: 'hidden',
width: '100%',
},
splitButtonBase: {
flex: 1,
paddingVertical: 10,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#007bff',
},
splitButtonLeft: {},
splitButtonRight: {},
buttonSeparator: {
width: 1,
backgroundColor: '#fff',
},
splitButtonText: {
color: 'white',
fontSize: 14,
fontWeight: '600',
},
addReviewButton: {
backgroundColor: '#28a745',
borderRadius: 5,
paddingVertical: 10,
alignItems: 'center',
marginTop: 10,
width: '100%',
},
addReviewButtonText: {
color: 'white',
fontSize: 15,
fontWeight: '600',
},
addPhotoButton: {
backgroundColor: '#6c757d',
borderRadius: 5,
paddingVertical: 10,
alignItems: 'center',
marginTop: 10,
width: '100%',
},
addPhotoButtonText: {
color: 'white',
fontSize: 15,
fontWeight: '600',
},
websiteText: {
fontSize: 14,
color: 'blue',
textDecorationLine: 'underline',
marginTop: 4,
textAlign: 'center',
marginBottom: 8,
},
directionsButton: {
backgroundColor: '#007bff',
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 25,
marginTop: 20,
minWidth: 150,
alignItems: 'center',
width: '100%',
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
marginTop: 15,
marginBottom: 8,
color: '#333',
textAlign: 'center',
},
reviewItem: {
backgroundColor: '#f9f9f9',
padding: 15,
borderRadius: 8,
marginBottom: 10,
borderWidth: 1,
borderColor: '#eee',
},
reviewRating: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 5,
color: '#007bff',
},
reviewDetail: {
fontSize: 14,
color: '#555',
marginBottom: 2,
},
reviewComment: {
fontSize: 14,
fontStyle: 'italic',
marginTop: 8,
color: '#666',
borderLeftWidth: 3,
borderLeftColor: '#ccc',
paddingLeft: 10,
},
reviewUser: {
fontSize: 12,
fontStyle: 'italic',
textAlign: 'right',
marginTop: 5,
color: '#888',
},
});
const styles = StyleSheet.create({
buttonContainer: {
position: 'absolute',
bottom: 20,
right: 20,
left: 20, // This ensures the button doesn't go edge-to-edge
alignItems: 'flex-end', // Aligns the button to the right
},
addMosqueButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#007AFF',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 25,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Remove fixed width and height to allow the button to expand
minWidth: 200, // Minimum width to ensure it's not too narrow
},
addMosqueText: {
color: 'white',
marginLeft: 8, // Add some space between icon and text
fontSize: 16, // Adjust as needed
},
container: { flex: 1 },
map: { flex: 1 },
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
prayerMarker: {
width: 30,
height: 40,
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 5,
},
prayerDot: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#1976D2',
borderWidth: 2,
borderColor: '#fff',
},
});