Files
boc/iom/quixzoom-app/screens/HomeScreen.tsx
T

333 lines
9.9 KiB
TypeScript
Raw Normal View History

/**
* HomeScreen — karta med live-uppdrag + snabbstatistik (intjänat idag)
* Integrerad med useLiveLocation() för kontinuerlig GPS-tracking
*/
import { useCallback, useEffect, useRef, useState } from 'react'
import {
View, Text, StyleSheet, TouchableOpacity,
ActivityIndicator, Platform,
} from 'react-native'
import MapView, { Marker, Region, PROVIDER_DEFAULT } from 'react-native-maps'
import { useNavigation } from '@react-navigation/native'
import { missions, wallet, type Mission, type WalletBalance } from '../lib/api'
import { useLiveLocation } from '../src/hooks/useLiveLocation'
import { useHeading } from '../src/hooks/useHeading'
import { useLocationStore } from '../src/stores/locationStore'
import { UserLocationMarker } from '../src/components/gps/UserLocationMarker'
import { FollowUserButton } from '../src/components/gps/FollowUserButton'
import { GpsDiagnostics } from '../src/components/gps/GpsDiagnostics'
const STOCKHOLM_ARCHIPELAGO: Region = {
latitude: 59.45,
longitude: 18.5,
latitudeDelta: 0.8,
longitudeDelta: 0.8,
}
const CATEGORY_EMOJI: Record<string, string> = {
infrastruktur: '🏗️',
vägar: '🛤️',
hamnar: '⚓',
bryggor: '🚢',
fritidshus: '🏠',
miljö: '🌿',
övrigt: '📍',
}
export function HomeScreen() {
const navigation = useNavigation<any>()
const mapRef = useRef<MapView>(null)
// Initialize live location tracking
useLiveLocation()
useHeading()
const {
currentPosition,
followUser,
setUserPannedMap,
locationAuthStatus,
} = useLocationStore()
const [region, setRegion] = useState<Region>(STOCKHOLM_ARCHIPELAGO)
const [nearbyMissions, setNearbyMissions] = useState<Mission[]>([])
const [balance, setBalance] = useState<WalletBalance | null>(null)
const [loadingMissions, setLoadingMissions] = useState(false)
const [showDiagnostics, setShowDiagnostics] = useState(false)
const [initialLocationSet, setInitialLocationSet] = useState(false)
// Center map on first position fix
useEffect(() => {
if (currentPosition && !initialLocationSet) {
const newRegion: Region = {
latitude: currentPosition.coords.latitude,
longitude: currentPosition.coords.longitude,
latitudeDelta: 0.5,
longitudeDelta: 0.5,
}
setRegion(newRegion)
mapRef.current?.animateToRegion(newRegion, 800)
fetchNearby(currentPosition.coords.latitude, currentPosition.coords.longitude)
setInitialLocationSet(true)
}
}, [currentPosition, initialLocationSet])
// Auto-follow user when followUser is true
useEffect(() => {
if (followUser && currentPosition && mapRef.current) {
const newRegion: Region = {
latitude: currentPosition.coords.latitude,
longitude: currentPosition.coords.longitude,
latitudeDelta: region.latitudeDelta,
longitudeDelta: region.longitudeDelta,
}
mapRef.current.animateToRegion(newRegion, 300)
}
}, [currentPosition, followUser, region.latitudeDelta, region.longitudeDelta])
// Wallet balance
useEffect(() => {
wallet.balance().then(setBalance).catch(() => {})
}, [])
const fetchNearby = useCallback(async (lat: number, lng: number) => {
setLoadingMissions(true)
try {
const data = await missions.nearby(lat, lng, 60)
setNearbyMissions(data)
} catch {
// silent
} finally {
setLoadingMissions(false)
}
}, [])
const onRegionChangeComplete = useCallback((r: Region) => {
setRegion(r)
fetchNearby(r.latitude, r.longitude)
}, [fetchNearby])
const onPanDrag = useCallback(() => {
setUserPannedMap(true)
}, [setUserPannedMap])
const availableSEK = balance ? ((balance.balance - balance.pending_payouts) / 100).toFixed(0) : '—'
const pendingSEK = balance ? (balance.pending_payouts / 100).toFixed(0) : '—'
return (
<View style={styles.container}>
{/* Map */}
<MapView
ref={mapRef}
style={styles.map}
provider={PROVIDER_DEFAULT}
initialRegion={STOCKHOLM_ARCHIPELAGO}
region={region}
onRegionChangeComplete={onRegionChangeComplete}
onPanDrag={onPanDrag}
showsUserLocation={false} // We use custom UserLocationMarker
showsMyLocationButton={false}
rotateEnabled={false} // Map stays north-up, marker rotates
>
{/* Custom user location marker with heading */}
{currentPosition && (
<Marker
coordinate={{
latitude: currentPosition.coords.latitude,
longitude: currentPosition.coords.longitude,
}}
anchor={{ x: 0.5, y: 0.5 }}
flat={true}
>
<UserLocationMarker />
</Marker>
)}
{nearbyMissions.map(m => (
<Marker
key={m.id}
coordinate={{ latitude: m.latitude, longitude: m.longitude }}
title={m.title}
description={`${(m.reward_amount / 100).toFixed(0)} SEK`}
onCalloutPress={() => navigation.navigate('MissionDetail', { missionId: m.id })}
>
<View style={styles.markerBubble}>
<Text style={styles.markerEmoji}>
{CATEGORY_EMOJI[m.category] ?? '📍'}
</Text>
<Text style={styles.markerReward}>
{(m.reward_amount / 100).toFixed(0)} kr
</Text>
</View>
</Marker>
))}
</MapView>
{/* Loading indicator */}
{loadingMissions && (
<View style={styles.loadingOverlay}>
<ActivityIndicator color="#6366f1" size="small" />
</View>
)}
{/* GPS Diagnostics toggle */}
<TouchableOpacity
style={styles.diagnosticsToggle}
onPress={() => setShowDiagnostics(prev => !prev)}
>
<Text style={styles.diagnosticsToggleText}>
{showDiagnostics ? '🔧' : '🛠️'}
</Text>
</TouchableOpacity>
{/* GPS Diagnostics overlay */}
{showDiagnostics && <GpsDiagnostics />}
{/* Stats card */}
<View style={styles.statsCard}>
<View style={styles.statItem}>
<Text style={styles.statLabel}>Tillgängligt</Text>
<Text style={styles.statValue}>{availableSEK} kr</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statLabel}>Under behandling</Text>
<Text style={[styles.statValue, { color: '#f59e0b' }]}>{pendingSEK} kr</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statLabel}>Uppdrag nära</Text>
<Text style={[styles.statValue, { color: '#10b981' }]}>{nearbyMissions.length}</Text>
</View>
</View>
{/* Follow user button */}
<View style={styles.followBtnContainer}>
<FollowUserButton />
</View>
{/* Location status indicator */}
{locationAuthStatus !== 'granted' && locationAuthStatus !== null && (
<View style={styles.locationWarning}>
<Text style={styles.locationWarningText}>
GPS-behörighet: {locationAuthStatus}
</Text>
</View>
)}
{/* Missions list button */}
<TouchableOpacity
style={styles.missionsBtn}
onPress={() => navigation.navigate('Uppdrag')}
>
<Text style={styles.missionsBtnText}>
🎯 {nearbyMissions.length} uppdrag nära dig
</Text>
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0A0A1B' },
map: { flex: 1 },
markerBubble: {
backgroundColor: '#6366f1',
borderRadius: 20,
paddingHorizontal: 10,
paddingVertical: 6,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.4,
shadowRadius: 4,
elevation: 4,
},
markerEmoji: { fontSize: 14 },
markerReward: { color: '#fff', fontSize: 11, fontWeight: '700' },
loadingOverlay: {
position: 'absolute',
top: Platform.OS === 'ios' ? 60 : 16,
right: 16,
backgroundColor: 'rgba(0,0,0,0.7)',
borderRadius: 20,
padding: 8,
},
diagnosticsToggle: {
position: 'absolute',
top: Platform.OS === 'ios' ? 60 : 16,
right: 60,
width: 36,
height: 36,
backgroundColor: 'rgba(10,10,27,0.9)',
borderRadius: 18,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.15)',
alignItems: 'center',
justifyContent: 'center',
},
diagnosticsToggleText: { fontSize: 16 },
statsCard: {
position: 'absolute',
top: Platform.OS === 'ios' ? 60 : 16,
left: 16,
right: 104,
flexDirection: 'row',
backgroundColor: 'rgba(10,10,27,0.92)',
borderRadius: 16,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
padding: 12,
alignItems: 'center',
},
statItem: { flex: 1, alignItems: 'center' },
statLabel: { color: 'rgba(255,255,255,0.45)', fontSize: 10, marginBottom: 2 },
statValue: { color: '#fff', fontSize: 15, fontWeight: '800' },
statDivider: { width: 1, height: 28, backgroundColor: 'rgba(255,255,255,0.1)' },
followBtnContainer: {
position: 'absolute',
bottom: 100,
right: 16,
},
locationWarning: {
position: 'absolute',
top: Platform.OS === 'ios' ? 120 : 76,
left: 16,
right: 16,
backgroundColor: 'rgba(245,158,11,0.2)',
borderRadius: 12,
padding: 10,
borderWidth: 1,
borderColor: 'rgba(245,158,11,0.4)',
alignItems: 'center',
},
locationWarningText: {
color: '#f59e0b',
fontSize: 12,
fontWeight: '600',
},
missionsBtn: {
position: 'absolute',
bottom: 32,
left: 16,
right: 16,
backgroundColor: '#6366f1',
borderRadius: 16,
paddingVertical: 16,
alignItems: 'center',
shadowColor: '#6366f1',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.4,
shadowRadius: 12,
elevation: 8,
},
missionsBtnText: { color: '#fff', fontWeight: '700', fontSize: 16 },
})