import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, Switch, ScrollView, TouchableOpacity, Platform, Alert, Image, ActivityIndicator, Modal, Dimensions, TextInput, // Needed for the new ReviewForm Button, // Needed for the new ReviewForm Linking, } from 'react-native'; import { Picker } from '@react-native-picker/picker'; import Slider from '@react-native-community/slider'; import { useRouter } from 'expo-router'; import * as Location from 'expo-location'; import ImageViewerModal from '../ImageViewerModal'; // Assuming this is still a separate file // --- Interfaces --- export interface ImageObject { id?: string; // ID should be optional for frontend if not always present or generated by backend url: string; note?: string; } export interface Review { ID: string; Rating?: number; // Calculated average (quiet+clean+private)/3 from backend, optional if not always present Quiet: number; Clean: number; Private: number; Comment?: string; // Optional: if you add a comment column to your DB later User?: string; // Optional: The reviewer's username, derived from a join with users table in backend // NEW fields in Review from your provided code CleanWudu?: number; ChildFriendly?: number; Safe?: number; } interface PrayerSpaceBase { id: string; name: string; latitude: number; longitude: number; address: string; type: 'Mosque' | 'Prayer Room' | 'Other' | 'Outdoor Space' | 'Multi-Faith Room'; womensSpace: boolean; wudu: boolean; openingHours: string; clean: number; // Average cleanliness rating from reviews // NEW fields in PrayerSpaceBase from your provided code cleanWudu: number; // Average wudu cleanliness rating from reviews quiet: number; // Average quietness rating from reviews privateness: number; // Average privateness rating from reviews childFriendly: number; // Average child friendly rating from reviews safe: number; // Average safety rating from reviews numberOfReviews: number; images: ImageObject[]; notes?: string; website: string; reviews: Review[]; // Array of detailed review objects for this space } // Function to map backend data to frontend PrayerSpaceBase structure function mapBackendSpaceToPrayerSpace(backendSpace: any): PrayerSpaceBase { const { Reviews = [], Images = [] } = backendSpace; const totalClean = Reviews.reduce((acc: number, r: Review) => acc + (r.Clean || 0), 0); const totalCleanWudu = Reviews.reduce((acc: number, r: Review) => acc + (r.CleanWudu || 0), 0); // NEW const totalQuiet = Reviews.reduce((acc: number, r: Review) => acc + (r.Quiet || 0), 0); const totalPrivate = Reviews.reduce((acc: number, r: Review) => acc + (r.Private || 0), 0); const totalChildFriendly = Reviews.reduce((acc: number, r: Review) => acc + (r.ChildFriendly || 0), 0); // NEW const totalSafe = Reviews.reduce((acc: number, r: Review) => acc + (r.Safe || 0), 0); // NEW const totalRating = Reviews.reduce((acc: number, r: Review) => acc + (r.Rating || 0), 0); const averageClean = Reviews.length ? (totalClean / Reviews.length) : 0; const averageCleanWudu = Reviews.length ? (totalCleanWudu / Reviews.length) : 0; // NEW const averageQuiet = Reviews.length ? (totalQuiet / Reviews.length) : 0; const averagePrivate = Reviews.length ? (totalPrivate / Reviews.length) : 0; const averageChildFriendly = Reviews.length ? (totalChildFriendly / Reviews.length) : 0; // NEW const averageSafe = Reviews.length ? (totalSafe / Reviews.length) : 0; // NEW const averageOverallRating = Reviews.length ? (totalRating / Reviews.length) : 0; return { id: backendSpace.ID, name: backendSpace.Name, latitude: backendSpace.Latitude, longitude: backendSpace.Longitude, address: backendSpace.Address, type: prayerSpaceTypeMap(backendSpace.LocationType), // Using prayerSpaceTypeMap womensSpace: backendSpace.WomensSpace, wudu: backendSpace.Wudu, openingHours: '', clean: averageClean, cleanWudu: averageCleanWudu, // NEW quiet: averageQuiet, privateness: averagePrivate, childFriendly: averageChildFriendly, // NEW safe: averageSafe, // NEW numberOfReviews: Reviews.length, images: Images.map((img: any) => ({ id: img.ID, url: convertToFullImageUrl(img.ImageURL), // Applying convertToFullImageUrl note: img.Notes, })) || [], // Ensure images is an array even if backend returns null/undefined notes: backendSpace.Notes, website: backendSpace.WebsiteURL, reviews: Reviews, }; } // --- Image URL Conversion --- function convertToFullImageUrl(imageUrl: string): string { // If it's already a full URL (starts with http:// or https://), return as is if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { return imageUrl; } // If it's a server path, convert it to a full URL // IMPORTANT: Replace this IP with your actual backend's accessible IP/domain if not running locally // or if your phone/emulator cannot reach 132.145.65.145 return `http://132.145.65.145:8080${imageUrl}`; } // --- Emoji Rating Helpers --- function averageRatingToEmoji(avg: number) { if (avg <= 0.5) { return "Unrated"; // If avg is 0 or less, return "Unrated" } if (avg < 1.66) { return "😞"; // closer to 1 (sad) } else if (avg < 2.33) { return "😐"; // closer to 2 (neutral) } else { return "😊"; // closer to 3 (happy) } } export function getListItemStyle(rating: number) { return { ...styles.listItemText, backgroundColor: getBackgroundColorForRating(rating), paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, marginBottom: 6, }; } function getBackgroundColorForRating(rating: number) { if (typeof rating !== 'number' || rating <= 0.5) { return '#e0e0e0'; // Darker gray for unrated (more visible) } if (rating < 1.66) { return '#ffcdd2'; // More saturated light red for sad (😞) } else if (rating < 2.33) { return '#ffe0b2'; // More saturated light orange for neutral (😐) } else { return '#c8e6c9'; // More saturated light green for happy (😊) } } function averageRatingToText(avg: number) { if (avg <= 0.5) { return "" } else { return avg.toFixed(1); } } interface EmojiRatingSelectorProps { label: string; value: number | null; onSelect: (value: number) => void; style?: object; // Allow custom styles } function getReviewDetailStyle(rating: number) { return { ...modalStyles.reviewDetail, backgroundColor: getBackgroundColorForRating(rating), paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, marginBottom: 4, }; } const EmojiRatingSelector: React.FC = ({ label, value, onSelect, style }) => { const emojis = [ { emoji: "😞", value: 1, label: "Poor" }, { emoji: "😐", value: 2, label: "Okay" }, { emoji: "😊", value: 3, label: "Great" } ]; return ( {label} {emojis.map((item) => ( onSelect(item.value)} > {item.emoji} {item.label} ))} ); }; // --- Prayer Space Type Map --- // --- Prayer Space Type Map --- // UPDATED: prayerSpaceTypeMap function to include new types for display function prayerSpaceTypeMap(str: string): 'Mosque' | 'Prayer Room' | 'Community Space' | 'Other' | 'Outdoor Space' | 'Multi-Faith Room' { switch (str) { case "mosque": return "Mosque" case "outdoor_space": // NEW Type return "Outdoor Space" case "multi_faith_room": // NEW Type return "Multi-Faith Room" case "other": return "Other" default: return "Other" // Fallback for unexpected values } } interface PrayerSpaceWithDistance extends PrayerSpaceBase { calculatedDistance: number | null; } interface Filters { showWomensSpace: boolean; showWudu: boolean; minRating: number; spaceType: ('Mosque' | 'Prayer Room' | 'Community Space' | 'All' | 'Other'); cleanRating?: number; cleanWuduRating?: number; quietRating?: number; privatenessRating?: number; childFriendlyRating?: number; safeRating?: number; } // --- Haversine Distance Calculation --- function getDistanceInKm(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371; // Radius of the earth in km const dLat = deg2rad(lat2 - lat1); const dLon = deg2rad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in km } function deg2rad(deg: number): number { return deg * (Math.PI / 180); } // --- ReviewsModal Component (Inline) --- interface ReviewsModalProps { visible: boolean; onClose: () => void; reviews: Review[]; spaceName: string; } const { width, height } = Dimensions.get('window'); const ReviewsModal: React.FC = ({ visible, onClose, reviews, spaceName }) => { return ( Reviews for {spaceName} {reviews.length === 0 ? ( No reviews yet. Be the first to leave one! ) : ( {reviews.map((review, index) => ( {/* Assuming Rating field exists, if not, calculate here */} {review.Rating !== undefined && review.Rating !== null && ( Overall Rating: ⭐ {review.Rating.toFixed(1)} )} {/* Using new fields in review display */} Cleanliness: {typeof review.Clean === 'number' ? averageRatingToEmoji(review.Clean) : '—'} Wudu Cleanliness: {typeof review.CleanWudu === 'number' ? averageRatingToEmoji(review.CleanWudu) : '—'} Quietness: {typeof review.Quiet === 'number' ? averageRatingToEmoji(review.Quiet) : '—'} Privacy: {typeof review.Private === 'number' ? averageRatingToEmoji(review.Private) : '—'} Child Friendliness: {typeof review.ChildFriendly === 'number' ? averageRatingToEmoji(review.ChildFriendly) : '—'} Safety: {typeof review.Safe === 'number' ? averageRatingToEmoji(review.Safe) : '—'} {review.Comment && "{review.Comment}"} {review.User && — {review.User}} ))} )} Close ); }; const modalStyles = StyleSheet.create({ centeredView: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', }, modalView: { margin: 20, backgroundColor: '#FFEEE7', // modal background borderRadius: 10, padding: 25, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 5, width: width * 0.9, maxHeight: height * 0.8, }, modalTitle: { fontSize: 22, fontWeight: 'bold', marginBottom: 20, color: '#333', textAlign: 'center', }, reviewsScrollView: { width: '100%', maxHeight: height * 0.6, marginBottom: 15, }, reviewItem: { backgroundColor: '#FFEEE7', // list item background 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', }, noReviewsText: { fontSize: 16, color: '#777', textAlign: 'center', paddingVertical: 20, }, closeButton: { backgroundColor: '#007bff', borderRadius: 8, paddingVertical: 12, paddingHorizontal: 25, marginTop: 10, minWidth: 120, alignItems: 'center', }, closeButtonText: { color: 'white', fontSize: 16, fontWeight: 'bold', }, }); // --- End ReviewsModal Component --- // --- ReviewForm Component (Inline - sends strings, matching your working ReviewScreen.tsx) --- interface ReviewFormProps { placeName: string; // Now expecting place NAME string onReviewSubmitted: () => void; } const HARDCODED_USERNAME = 'testuser'; // Hardcoded username string, matching your seed user const ReviewForm: React.FC = ({ placeName, onReviewSubmitted }) => { const [quiet, setQuiet] = useState(null); const [clean, setClean] = useState(null); const [privacy, setPrivacy] = useState(null); const [cleanWudu, setCleanWudu] = useState(null); // NEW const [childFriendly, setChildFriendly] = useState(null); // NEW const [safe, setSafe] = useState(null); // NEW const [comment, setComment] = useState(''); // Uncomment if you add comment to DB // Updated isValidRating for new emoji scale (1-3) const isValidRating = (val: number | null): boolean => { return val !== null && val >= 1 && val <= 3; }; const handleSubmit = async () => { // Validate all NEW rating fields if (!quiet || !clean || !privacy || !cleanWudu || !childFriendly || !safe) { Alert.alert('Missing Info', 'Please fill in all rating fields.'); return; } // Validate all NEW rating fields with isValidRating if (![quiet, clean, privacy, cleanWudu, childFriendly, safe].every(isValidRating)) { Alert.alert('Invalid Input', 'Please select a rating for each category (1-3).'); return; } // Constructing the payload with username and place name strings const payload = { user: HARDCODED_USERNAME, // Hardcoded username string place: placeName, // Place NAME string quiet: quiet, clean: clean, private: privacy, cleanWudu: cleanWudu, // NEW childFriendly: childFriendly, // NEW safe: safe, // NEW notes: comment, // Uncomment if you add comment to DB }; console.log("Sending review payload (strings and new ratings):", payload); try { const response = await fetch('http://132.145.65.145:8080/reviews/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 data = await response.json(); Alert.alert('Success', data.message || 'Review submitted!'); // Reset form (including new fields) setQuiet(null); setClean(null); setPrivacy(null); setCleanWudu(null); // NEW setChildFriendly(null); // NEW setSafe(null); // NEW onReviewSubmitted(); } catch (error: any) { Alert.alert('Error', `Could not send review: ${error.message || error}`); console.error("Review submission error:", error); } }; return ( Submit a Review for {placeName} {/* Using EmojiRatingSelector for all ratings */}