/** * quiXzoom Native App * iOS/Android field data collection * Mobile-first design */ import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Alert, ActivityIndicator, SafeAreaView, StatusBar, } from 'react-native'; import { Camera, useCameraDevices } from 'react-native-vision-camera'; import Geolocation from '@react-native-community/geolocation'; import AsyncStorage from '@react-native-async-storage/async-storage'; // API Configuration const API_URL = 'https://api.quixzoom.com'; const API_KEY = 'YOUR_API_KEY'; /** * quiXzoom App Component */ export default function quiXzoomApp() { // State const [hasPermission, setHasPermission] = useState(false); const [isCapturing, setIsCapturing] = useState(false); const [capturedImage, setCapturedImage] = useState(null); const [location, setLocation] = useState(null); const [evidence, setEvidence] = useState(null); const [missions, setMissions] = useState([]); const [activeMission, setActiveMission] = useState(null); const [isOffline, setIsOffline] = useState(false); const [queueCount, setQueueCount] = useState(0); // Camera ref const camera = useRef(null); const devices = useCameraDevices(); const device = devices.back; // Initialize useEffect(() => { checkPermissions(); loadMissions(); loadQueue(); }, []); // Check camera and location permissions const checkPermissions = async () => { const cameraStatus = await Camera.requestCameraPermission(); const locationStatus = await Geolocation.requestAuthorization(); setHasPermission(cameraStatus === 'authorized'); }; // Load missions from API const loadMissions = async () => { try { const response = await fetch(`${API_URL}/missions`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); const data = await response.json(); setMissions(data.missions || []); } catch (error) { console.log('Offline mode - using cached missions'); setIsOffline(true); } }; // Load offline queue const loadQueue = async () => { const queue = await AsyncStorage.getItem('observationQueue'); if (queue) { setQueueCount(JSON.parse(queue).length); } }; // Capture photo const capturePhoto = async () => { if (!camera.current) return; setIsCapturing(true); try { // Take photo const photo = await camera.current.takePhoto({ qualityPrioritization: 'quality', flash: 'auto', }); // Get GPS location Geolocation.getCurrentPosition( async (position) => { const loc = { latitude: position.coords.latitude, longitude: position.coords.longitude, altitude: position.coords.altitude, accuracy: position.coords.accuracy, heading: position.coords.heading, timestamp: new Date().toISOString(), }; setLocation(loc); setCapturedImage(photo.path); // Extract evidence await extractEvidence(photo.path, loc); }, (error) => { Alert.alert('GPS Error', 'Could not get location'); setIsCapturing(false); }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 } ); } catch (error) { Alert.alert('Error', 'Failed to capture photo'); setIsCapturing(false); } }; // Extract evidence from image const extractEvidence = async (imagePath, loc) => { try { // Create form data const formData = new FormData(); formData.append('image', { uri: imagePath, type: 'image/jpeg', name: 'observation.jpg', }); formData.append('latitude', loc.latitude.toString()); formData.append('longitude', loc.longitude.toString()); formData.append('altitude', loc.altitude?.toString() || '0'); // Send to API const response = await fetch(`${API_URL}/visual-geolocation/analyze`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'multipart/form-data', }, body: formData, }); const data = await response.json(); setEvidence(data); setIsCapturing(false); // Save to queue if offline if (isOffline) { await saveToQueue({ imagePath, location: loc, evidence: data, timestamp: new Date().toISOString(), }); } } catch (error) { console.log('Evidence extraction failed, saving to queue'); await saveToQueue({ imagePath, location: loc, evidence: null, timestamp: new Date().toISOString(), }); setIsCapturing(false); } }; // Save to offline queue const saveToQueue = async (observation) => { const queue = await AsyncStorage.getItem('observationQueue'); const observations = queue ? JSON.parse(queue) : []; observations.push(observation); await AsyncStorage.setItem('observationQueue', JSON.stringify(observations)); setQueueCount(observations.length); }; // Sync offline queue const syncQueue = async () => { const queue = await AsyncStorage.getItem('observationQueue'); if (!queue) return; const observations = JSON.parse(queue); let synced = 0; for (const obs of observations) { try { // Re-send to API const formData = new FormData(); formData.append('image', { uri: obs.imagePath, type: 'image/jpeg', name: 'observation.jpg', }); formData.append('latitude', obs.location.latitude.toString()); formData.append('longitude', obs.location.longitude.toString()); await fetch(`${API_URL}/visual-geolocation/analyze`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'multipart/form-data', }, body: formData, }); synced++; } catch (error) { console.log('Sync failed for observation'); } } // Clear synced observations const remaining = observations.slice(synced); await AsyncStorage.setItem('observationQueue', JSON.stringify(remaining)); setQueueCount(remaining.length); Alert.alert('Sync Complete', `${synced} observations synced`); }; // Submit observation const submitObservation = async () => { if (!capturedImage || !location) return; try { const response = await fetch(`${API_URL}/observations`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ image_url: capturedImage, latitude: location.latitude, longitude: location.longitude, altitude: location.altitude, evidence: evidence, mission_id: activeMission?.id, timestamp: new Date().toISOString(), }), }); if (response.ok) { Alert.alert('Success', 'Observation submitted!'); setCapturedImage(null); setEvidence(null); setLocation(null); } else { throw new Error('Submit failed'); } } catch (error) { Alert.alert('Error', 'Saved to queue for later sync'); await saveToQueue({ imagePath: capturedImage, location, evidence, timestamp: new Date().toISOString(), }); } }; // Render camera view const renderCamera = () => { if (device == null) return ; return ( {/* Overlay */} {/* GPS Status */} {location ? '📍 GPS Ready' : '📍 Getting GPS...'} {/* Mission Info */} {activeMission && ( 🎯 {activeMission.title} ${activeMission.reward_usd} )} {/* Queue Status */} {queueCount > 0 && ( 📤 {queueCount} in queue )} {/* Capture Button */} {isCapturing ? ( ) : ( )} ); }; // Render preview const renderPreview = () => { return ( {/* Location Info */} {location && ( 📍 Location Lat: {location.latitude.toFixed(6)} Lng: {location.longitude.toFixed(6)} Accuracy: ±{location.accuracy?.toFixed(0)}m )} {/* Evidence */} {evidence && ( 🔍 Evidence Visual Objects: {evidence.evidence_summary?.visual_objects || 0} Text Detections: {evidence.evidence_summary?.text_detections || 0} Confidence: {(evidence.confidence_report?.overall * 100).toFixed(0)}% )} {/* Actions */} ✓ Submit Observation { setCapturedImage(null); setEvidence(null); }} > ↻ Retake ); }; // Render missions list const renderMissions = () => { return ( 🎯 Available Missions {missions.map((mission) => ( setActiveMission(mission)} > {mission.title} {mission.description} ${mission.reward_usd} {new Date(mission.deadline).toLocaleDateString()} ))} {missions.length === 0 && ( No missions available )} ); }; // Main render return ( {/* Header */} quiXzoom {isOffline && ( OFFLINE )} {queueCount > 0 && ( 📤 {queueCount} )} {/* Content */} {capturedImage ? renderPreview() : renderCamera()} {/* Bottom Navigation */} 📷 Capture 🎯 Missions 📊 Stats ⚙️ Settings ); } // Styles const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, backgroundColor: '#1a1a1a', }, headerTitle: { fontSize: 20, fontWeight: 'bold', color: '#fff', }, headerRight: { flexDirection: 'row', alignItems: 'center', }, offlineBadge: { color: '#ff9500', fontSize: 12, fontWeight: 'bold', marginRight: 8, }, cameraContainer: { flex: 1, position: 'relative', }, camera: { flex: 1, }, overlay: { position: 'absolute', top: 0, left: 0, right: 0, padding: 16, }, gpsBadge: { backgroundColor: 'rgba(0,0,0,0.7)', padding: 8, borderRadius: 8, alignSelf: 'flex-start', marginBottom: 8, }, gpsText: { color: '#fff', fontSize: 14, }, missionBadge: { backgroundColor: 'rgba(0,122,255,0.8)', padding: 8, borderRadius: 8, alignSelf: 'flex-start', marginBottom: 8, }, missionText: { color: '#fff', fontSize: 14, fontWeight: 'bold', }, missionReward: { color: '#fff', fontSize: 12, }, queueBadge: { backgroundColor: 'rgba(255,149,0,0.8)', padding: 8, borderRadius: 8, alignSelf: 'flex-start', }, queueText: { color: '#fff', fontSize: 14, }, captureButton: { position: 'absolute', bottom: 100, alignSelf: 'center', width: 80, height: 80, borderRadius: 40, backgroundColor: 'rgba(255,255,255,0.3)', justifyContent: 'center', alignItems: 'center', }, captureInner: { width: 60, height: 60, borderRadius: 30, backgroundColor: '#fff', }, previewContainer: { flex: 1, backgroundColor: '#000', }, previewImage: { width: '100%', height: 300, resizeMode: 'cover', }, infoCard: { backgroundColor: '#1a1a1a', margin: 16, padding: 16, borderRadius: 12, }, infoTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 8, }, infoText: { color: '#ccc', fontSize: 14, marginBottom: 4, }, actionButtons: { flexDirection: 'row', justifyContent: 'space-around', margin: 16, }, submitButton: { backgroundColor: '#34c759', padding: 16, borderRadius: 12, flex: 1, marginRight: 8, }, submitText: { color: '#fff', fontSize: 16, fontWeight: 'bold', textAlign: 'center', }, retakeButton: { backgroundColor: '#ff3b30', padding: 16, borderRadius: 12, flex: 1, marginLeft: 8, }, retakeText: { color: '#fff', fontSize: 16, fontWeight: 'bold', textAlign: 'center', }, missionsContainer: { flex: 1, backgroundColor: '#000', padding: 16, }, sectionTitle: { color: '#fff', fontSize: 20, fontWeight: 'bold', marginBottom: 16, }, missionCard: { backgroundColor: '#1a1a1a', padding: 16, borderRadius: 12, marginBottom: 12, }, activeMission: { borderColor: '#007aff', borderWidth: 2, }, missionTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 4, }, missionDescription: { color: '#ccc', fontSize: 14, marginBottom: 8, }, missionFooter: { flexDirection: 'row', justifyContent: 'space-between', }, missionReward: { color: '#34c759', fontSize: 16, fontWeight: 'bold', }, missionDeadline: { color: '#999', fontSize: 14, }, emptyText: { color: '#666', fontSize: 16, textAlign: 'center', marginTop: 32, }, bottomNav: { flexDirection: 'row', justifyContent: 'space-around', padding: 16, backgroundColor: '#1a1a1a', borderTopWidth: 1, borderTopColor: '#333', }, navButton: { alignItems: 'center', }, navText: { color: '#fff', fontSize: 12, }, });