199 lines
5.0 KiB
TypeScript
199 lines
5.0 KiB
TypeScript
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'
|
|
import { useState, useEffect } from 'react'
|
|
import { LinearGradient } from 'expo-linear-gradient'
|
|
import { Ionicons } from '@expo/vector-icons'
|
|
import * as Location from 'expo-location'
|
|
import { theme } from '../styles/theme'
|
|
import { isWithinGeofence, calculateDistance, getFormattedDistance, GYM_LOCATION } from '../services/geofencing'
|
|
|
|
export interface GeofenceStatusProps {
|
|
onStatusChange?: (isInside: boolean) => void
|
|
}
|
|
|
|
export const GeofenceStatus = ({ onStatusChange }: GeofenceStatusProps) => {
|
|
const [loading, setLoading] = useState(false)
|
|
const [isInside, setIsInside] = useState(false)
|
|
const [distance, setDistance] = useState<string>('--')
|
|
const [permission, setPermission] = useState<string>('undetermined')
|
|
const [lastCheck, setLastCheck] = useState<Date | null>(null)
|
|
|
|
const checkGeofence = async () => {
|
|
try {
|
|
setLoading(true)
|
|
|
|
// Request location permission
|
|
const { status } = await Location.requestForegroundPermissionsAsync()
|
|
setPermission(status)
|
|
|
|
if (status !== 'granted') {
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
// Get current location
|
|
const location = await Location.getCurrentPositionAsync({
|
|
accuracy: Location.Accuracy.Balanced,
|
|
})
|
|
|
|
const { latitude, longitude } = location.coords
|
|
|
|
// Check if within geofence
|
|
const within = isWithinGeofence(latitude, longitude, 500)
|
|
setIsInside(within)
|
|
onStatusChange?.(within)
|
|
|
|
// Calculate distance
|
|
const dist = calculateDistance(
|
|
latitude,
|
|
longitude,
|
|
GYM_LOCATION.latitude,
|
|
GYM_LOCATION.longitude
|
|
)
|
|
setDistance(getFormattedDistance(dist))
|
|
setLastCheck(new Date())
|
|
} catch (error) {
|
|
console.error('Geofence check error:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
checkGeofence()
|
|
// Auto-check every 30 seconds
|
|
const interval = setInterval(checkGeofence, 30000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<LinearGradient
|
|
colors={
|
|
isInside
|
|
? ['#10b981', '#6ee7b7', '#a7f3d0']
|
|
: ['#64748b', '#94a3b8', '#cbd5e1']
|
|
}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.card}
|
|
>
|
|
<View style={styles.header}>
|
|
<View style={styles.titleRow}>
|
|
<Ionicons
|
|
name={isInside ? 'location-sharp' : 'location'}
|
|
size={24}
|
|
color="#fff"
|
|
/>
|
|
<Text style={styles.title}>
|
|
{isInside ? 'At Gym' : 'Away from Gym'}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={checkGeofence}
|
|
disabled={loading}
|
|
style={styles.refreshButton}
|
|
>
|
|
{loading ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<Ionicons name="refresh" size={20} color="#fff" />
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.content}>
|
|
<View style={styles.stat}>
|
|
<Text style={styles.statLabel}>Distance</Text>
|
|
<Text style={styles.statValue}>{distance}</Text>
|
|
</View>
|
|
|
|
{lastCheck && (
|
|
<Text style={styles.lastCheck}>
|
|
Last check: {lastCheck.toLocaleTimeString()}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
|
|
{permission !== 'granted' && (
|
|
<View style={styles.permissionAlert}>
|
|
<Ionicons name="alert-circle" size={16} color="#fff" />
|
|
<Text style={styles.permissionText}>
|
|
Location permission required
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</LinearGradient>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
},
|
|
card: {
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
overflow: 'hidden',
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 12,
|
|
},
|
|
titleRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
title: {
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
color: '#fff',
|
|
},
|
|
refreshButton: {
|
|
padding: 8,
|
|
borderRadius: 8,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
},
|
|
content: {
|
|
gap: 12,
|
|
},
|
|
stat: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
statLabel: {
|
|
fontSize: 14,
|
|
color: 'rgba(255, 255, 255, 0.8)',
|
|
fontWeight: '500',
|
|
},
|
|
statValue: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#fff',
|
|
},
|
|
lastCheck: {
|
|
fontSize: 12,
|
|
color: 'rgba(255, 255, 255, 0.7)',
|
|
marginTop: 4,
|
|
},
|
|
permissionAlert: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
marginTop: 12,
|
|
paddingTop: 12,
|
|
borderTopWidth: 1,
|
|
borderTopColor: 'rgba(255, 255, 255, 0.3)',
|
|
},
|
|
permissionText: {
|
|
fontSize: 12,
|
|
color: '#fff',
|
|
fontWeight: '500',
|
|
},
|
|
})
|