/**
* 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,
},
});