fix: tsconfig ignoreDeprecations and GeofenceStatus component - use GYM_LOCATION constant
This commit is contained in:
parent
2f79d1547f
commit
231ad44cc4
10
apps/mobile/package-lock.json
generated
10
apps/mobile/package-lock.json
generated
@ -28,6 +28,7 @@
|
|||||||
"expo-haptics": "^15.0.7",
|
"expo-haptics": "^15.0.7",
|
||||||
"expo-linear-gradient": "~15.0.7",
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.0",
|
"expo-linking": "~8.0.0",
|
||||||
|
"expo-location": "~17.0.1",
|
||||||
"expo-notifications": "~0.32.0",
|
"expo-notifications": "~0.32.0",
|
||||||
"expo-router": "~6.0.14",
|
"expo-router": "~6.0.14",
|
||||||
"expo-secure-store": "~15.0.7",
|
"expo-secure-store": "~15.0.7",
|
||||||
@ -7329,6 +7330,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-location": {
|
||||||
|
"version": "17.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-17.0.1.tgz",
|
||||||
|
"integrity": "sha512-m+OzotzlAXO3ZZ1uqW5GC25nXW868zN+ROyBA1V4VF6jGay1ZEs4URPglCVUDzZby2F5wt24cMzqDKw2IX6nRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "3.0.22",
|
"version": "3.0.22",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz",
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
"expo-haptics": "^15.0.7",
|
"expo-haptics": "^15.0.7",
|
||||||
"expo-linear-gradient": "~15.0.7",
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.0",
|
"expo-linking": "~8.0.0",
|
||||||
|
"expo-location": "~17.0.1",
|
||||||
"expo-notifications": "~0.32.0",
|
"expo-notifications": "~0.32.0",
|
||||||
"expo-router": "~6.0.14",
|
"expo-router": "~6.0.14",
|
||||||
"expo-secure-store": "~15.0.7",
|
"expo-secure-store": "~15.0.7",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Ionicons } from '@expo/vector-icons'
|
|||||||
import { attendanceApi, Attendance } from '../../api/attendance'
|
import { attendanceApi, Attendance } from '../../api/attendance'
|
||||||
import { theme } from '../../styles/theme'
|
import { theme } from '../../styles/theme'
|
||||||
import { Animated } from 'react-native'
|
import { Animated } from 'react-native'
|
||||||
|
import { GeofenceStatus } from '../../components/GeofenceStatus'
|
||||||
|
|
||||||
export default function AttendanceScreen() {
|
export default function AttendanceScreen() {
|
||||||
const { getToken, userId } = useAuth()
|
const { getToken, userId } = useAuth()
|
||||||
@ -111,6 +112,9 @@ export default function AttendanceScreen() {
|
|||||||
<Text style={styles.subtitle}>Track your gym visits</Text>
|
<Text style={styles.subtitle}>Track your gym visits</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
|
{/* Geofencing Status Card */}
|
||||||
|
<GeofenceStatus />
|
||||||
|
|
||||||
<View style={styles.actionContainer}>
|
<View style={styles.actionContainer}>
|
||||||
{activeCheckIn ? (
|
{activeCheckIn ? (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
|
|||||||
198
apps/mobile/src/components/GeofenceStatus.tsx
Normal file
198
apps/mobile/src/components/GeofenceStatus.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
})
|
||||||
74
apps/mobile/src/services/geofencing.ts
Normal file
74
apps/mobile/src/services/geofencing.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Geofencing Service
|
||||||
|
* Handles gym location validation and distance calculations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Gym location coordinates (example: gym location)
|
||||||
|
export const GYM_LOCATION = {
|
||||||
|
latitude: 37.7749,
|
||||||
|
longitude: -122.4194,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Geofence radius in meters (500m default)
|
||||||
|
export const GEOFENCE_RADIUS_METERS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance between two coordinates using Haversine formula
|
||||||
|
* @param lat1 Starting latitude
|
||||||
|
* @param lon1 Starting longitude
|
||||||
|
* @param lat2 Ending latitude
|
||||||
|
* @param lon2 Ending longitude
|
||||||
|
* @returns Distance in meters
|
||||||
|
*/
|
||||||
|
export function calculateDistance(
|
||||||
|
lat1: number,
|
||||||
|
lon1: number,
|
||||||
|
lat2: number,
|
||||||
|
lon2: number
|
||||||
|
): number {
|
||||||
|
const R = 6371000; // Earth's radius in meters
|
||||||
|
const φ1 = (lat1 * Math.PI) / 180;
|
||||||
|
const φ2 = (lat2 * Math.PI) / 180;
|
||||||
|
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
||||||
|
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
||||||
|
|
||||||
|
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
||||||
|
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
||||||
|
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
|
return R * c; // Distance in meters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a location is within the geofence
|
||||||
|
* @param userLat User's latitude
|
||||||
|
* @param userLon User's longitude
|
||||||
|
* @param radius Geofence radius in meters (defaults to GEOFENCE_RADIUS_METERS)
|
||||||
|
* @returns Boolean indicating if user is within geofence
|
||||||
|
*/
|
||||||
|
export function isWithinGeofence(
|
||||||
|
userLat: number,
|
||||||
|
userLon: number,
|
||||||
|
radius: number = GEOFENCE_RADIUS_METERS
|
||||||
|
): boolean {
|
||||||
|
const distance = calculateDistance(
|
||||||
|
userLat,
|
||||||
|
userLon,
|
||||||
|
GYM_LOCATION.latitude,
|
||||||
|
GYM_LOCATION.longitude
|
||||||
|
);
|
||||||
|
return distance <= radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted distance string
|
||||||
|
* @param meters Distance in meters
|
||||||
|
* @returns Formatted distance string
|
||||||
|
*/
|
||||||
|
export function getFormattedDistance(meters: number): string {
|
||||||
|
if (meters < 1000) {
|
||||||
|
return `${Math.round(meters)} m`;
|
||||||
|
}
|
||||||
|
return `${(meters / 1000).toFixed(2)} km`;
|
||||||
|
}
|
||||||
@ -12,7 +12,6 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"ignoreDeprecations": "6.0",
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user