377 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react'; // ADDED useEffect
import { // Ensure all necessary components are imported
View,
Text,
TouchableOpacity,
Image,
Alert,
StyleSheet,
TextInput,
ScrollView,
ActivityIndicator,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams } from 'expo-router'; // ADDED: Import useLocalSearchParams
interface UploadResponse {
success: boolean;
message: string;
imageUrl?: string;
}
export default function UploadTab() {
// Get parameters from the route
const params = useLocalSearchParams();
// Ensure placeId from params is treated as a string, provide fallback if not present
const initialPlaceId = typeof params.placeId === 'string' ? params.placeId : '';
const initialPlaceName = typeof params.placeName === 'string' ? params.placeName : 'Selected Place'; // For display
const [selectedImage, setSelectedImage] = useState<string | null>(null);
// Initialize placeId state with the value from params
const [placeId, setPlaceId] = useState<string>(initialPlaceId);
const [placeNameDisplay, setPlaceNameDisplay] = useState<string>(initialPlaceName); // State for displaying name
const [notes, setNotes] = useState<string>('');
const [uploading, setUploading] = useState<boolean>(false);
const BACKEND_URL = 'http://132.145.65.145:8080';
// Use useEffect to update placeId and placeNameDisplay if params change
// This handles cases where the component might already be mounted and params are updated
useEffect(() => {
if (typeof params.placeId === 'string' && params.placeId !== placeId) {
setPlaceId(params.placeId);
}
if (typeof params.placeName === 'string' && params.placeName !== placeNameDisplay) {
setPlaceNameDisplay(params.placeName);
}
}, [params.placeId, params.placeName]); // Depend on params.placeId and placeName
const pickImage = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission Required', 'Sorry, we need camera roll permissions to upload images.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, // Use MediaTypeOptions.Images directly
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
allowsMultipleSelection: false,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
setSelectedImage(result.assets[0].uri);
}
};
const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission Required', 'Sorry, we need camera permissions to take photos.');
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
quality: 0.8,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
setSelectedImage(result.assets[0].uri);
}
};
const uploadImage = async () => {
if (!selectedImage) {
Alert.alert('Error', 'Please select an image first');
return;
}
if (!placeId) { // Ensure placeId is not empty after potential navigation
Alert.alert('Error', 'Place ID is missing. Please select a mosque first.');
return;
}
setUploading(true);
try {
console.log('Uploading for placeId:', placeId);
console.log('Notes:', notes);
const formData = new FormData();
const imageFile = {
uri: selectedImage,
type: 'image/jpeg',
name: 'image.jpg',
} as any;
formData.append('image', imageFile);
formData.append('place_id', placeId.trim()); // Ensure this matches backend expected field name
formData.append('Notes', notes.trim()); // Ensure this matches backend expected field name
console.log('Sending form data with keys:', Array.from(formData.keys()));
for (let [key, value] of formData.entries()) {
console.log(key, value);
}
const response = await fetch(`${BACKEND_URL}/images/upload`, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
// DO NOT set 'Content-Type' for FormData, fetch sets it automatically with boundary
},
});
let result: UploadResponse;
const responseCopy = response.clone(); // Clone response to read body twice if needed
try {
result = await response.json();
} catch (error) {
const text = await responseCopy.text();
throw new Error(text || "Unknown error parsing JSON response");
}
if (response.ok && result.success) {
Alert.alert('Success', 'Image uploaded successfully!');
setSelectedImage(null);
setPlaceId(initialPlaceId); // Reset to initial placeId from params, or empty if not from params
setPlaceNameDisplay(initialPlaceName); // Reset display name
setNotes('');
} else {
Alert.alert('Upload Failed', result.message || 'Failed to upload image. Please check backend logs.');
}
} catch (error) {
console.error('Upload error:', error);
Alert.alert('Error', `Upload failed: ${error.message || 'Network error'}.`);
} finally {
setUploading(false);
}
};
const clearImage = () => {
setSelectedImage(null);
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
<Text style={styles.title}>Upload Image</Text>
{/* Display Place Name (User friendly) */}
<Text style={styles.placeNameDisplay}>For: {placeNameDisplay || 'No place selected'}</Text>
{/* Image Selection Section */}
<View style={styles.imageSection}>
{selectedImage ? (
<View style={styles.imageContainer}>
<Image source={{ uri: selectedImage }} style={styles.selectedImage} />
<TouchableOpacity style={styles.clearButton} onPress={clearImage}>
<Ionicons name="close-circle" size={24} color="#ff4444" />
</TouchableOpacity>
</View>
) : (
<View style={styles.placeholderContainer}>
<Ionicons name="image-outline" size={64} color="#ccc" />
<Text style={styles.placeholderText}>No image selected</Text>
</View>
)}
</View>
{/* Image Selection Buttons */}
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.actionButton} onPress={pickImage}>
<Ionicons name="images-outline" size={20} color="#007AFF" />
<Text style={styles.buttonText}>Gallery</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton} onPress={takePhoto}>
<Ionicons name="camera-outline" size={20} color="#007AFF" />
<Text style={styles.buttonText}>Camera</Text>
</TouchableOpacity>
</View>
{/* Form Section */}
<View style={styles.formSection}>
<Text style={styles.label}>Notes</Text>
<TextInput
style={[styles.input, styles.notesInput]}
value={notes}
onChangeText={setNotes}
placeholder="Add notes about this image (optional)"
placeholderTextColor="#999"
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
{/* Upload Button */}
<TouchableOpacity
style={[styles.uploadButton, (!selectedImage || uploading || !placeId) && styles.uploadButtonDisabled]} // Disable if placeId is missing
onPress={uploadImage}
disabled={!selectedImage || uploading || !placeId} // Disable if placeId is missing
>
{uploading ? (
<ActivityIndicator color="#fff" />
) : (
<>
<Ionicons name="cloud-upload-outline" size={20} color="#fff" />
<Text style={styles.uploadButtonText}>Upload Image</Text>
</>
)}
</TouchableOpacity>
{/* Instructions */}
<View style={styles.instructionsContainer}>
<Text style={styles.instructionsTitle}>Instructions:</Text>
<Text style={styles.instructionsText}>
1. Select an image from your gallery or take a photo{'\n'}
2. Add optional notes{'\n'}
3. Tap Upload
</Text>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
contentContainer: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
color: '#333',
},
placeNameDisplay: { // New style to display the place name
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
textAlign: 'center',
color: '#555',
},
imageSection: {
marginBottom: 20,
},
imageContainer: {
position: 'relative',
alignItems: 'center',
},
selectedImage: {
width: 300,
height: 200,
borderRadius: 10,
resizeMode: 'cover',
},
clearButton: {
position: 'absolute',
top: 10,
right: 10,
backgroundColor: '#fff',
borderRadius: 12,
},
placeholderContainer: {
height: 200,
backgroundColor: '#f0f0f0',
borderRadius: 10,
borderWidth: 2,
borderColor: '#ddd',
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
},
placeholderText: {
marginTop: 10,
color: '#999',
fontSize: 16,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 30,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#e3f2fd',
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#007AFF',
},
buttonText: {
marginLeft: 8,
color: '#007AFF',
fontSize: 16,
fontWeight: '500',
},
formSection: {
marginBottom: 30,
},
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,
},
readOnlyInput: { // NEW STYLE for read-only text input
backgroundColor: '#e9ecef', // Lighter background for read-only
color: '#6c757d', // Slightly greyed out text
},
notesInput: {
height: 80,
paddingTop: 12,
},
uploadButton: {
backgroundColor: '#28a745',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 15,
borderRadius: 8,
marginBottom: 30,
},
uploadButtonDisabled: {
backgroundColor: '#ccc',
},
uploadButtonText: {
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
marginLeft: 8,
},
instructionsContainer: {
backgroundColor: '#fff',
padding: 15,
borderRadius: 8,
borderLeftWidth: 4,
borderLeftColor: '#007AFF',
},
instructionsTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
},
instructionsText: {
fontSize: 14,
color: '#666',
lineHeight: 20,
},
});