Add profile screen and fix map's motion
parent
6db5b1355b
commit
75d43a3cd9
2
app.json
2
app.json
|
|
@ -6,7 +6,7 @@
|
|||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "batteryasaservice",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"userInterfaceStyle": "light",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
|
|
|
|||
|
|
@ -6,22 +6,24 @@ import {
|
|||
} from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SemiCircleProgress from "../../components/home/SemiCircleProgress";
|
||||
import { Text, View } from "@/components/Themed";
|
||||
import { Text, View } from "react-native";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import Profile from "../../components/home/Profile";
|
||||
import CustomerCareIcon from "../../assets/icons/customer-care.svg";
|
||||
import ServiceReminderCard from "@/components/home/ServiceReminderCard";
|
||||
import MetricCard from "@/components/home/MetricCard";
|
||||
import PaymentDueCard from "@/components/home/PaymentDueCard";
|
||||
import MapView, { Marker } from "react-native-maps";
|
||||
import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
|
||||
import BatteryWarrantyCard from "@/components/home/BatteryWarrantyCars";
|
||||
import CustomerSupportModal from "@/components/home/CustomerSupportModal";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/store";
|
||||
import LocationOff from "@/assets/icons/location_off.svg";
|
||||
import { Linking } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import ProfileImage from "@/components/home/Profile";
|
||||
import { calculateBearing, calculateDistance } from "@/utils/Map";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -30,6 +32,53 @@ export default function HomeScreen() {
|
|||
const { SoC, SoH, chargingState, lat, lon, loading, error } = useSelector(
|
||||
(state: RootState) => state.telemetry
|
||||
);
|
||||
const [prevPosition, setPrevPosition] = useState<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
} | null>(null);
|
||||
const [bearing, setBearing] = useState<number>(0);
|
||||
|
||||
const step = 0.001;
|
||||
useEffect(() => {
|
||||
if (lat && lon) {
|
||||
if (prevPosition) {
|
||||
// Calculate bearing between prev and current position
|
||||
const newBearing = calculateBearing(
|
||||
prevPosition.lat,
|
||||
prevPosition.lon,
|
||||
lat,
|
||||
lon
|
||||
);
|
||||
setBearing(newBearing);
|
||||
}
|
||||
setPrevPosition({ lat, lon }); // Update previous position
|
||||
}
|
||||
}, [lat, lon]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lat && lon) {
|
||||
if (prevPosition) {
|
||||
const distance = calculateDistance(
|
||||
prevPosition.lat,
|
||||
prevPosition.lon,
|
||||
lat,
|
||||
lon
|
||||
);
|
||||
if (distance > 5) {
|
||||
// Only update bearing if moved >5 meters
|
||||
const newBearing = calculateBearing(
|
||||
prevPosition.lat,
|
||||
prevPosition.lon,
|
||||
lat,
|
||||
lon
|
||||
);
|
||||
setBearing(newBearing);
|
||||
}
|
||||
}
|
||||
setPrevPosition({ lat, lon });
|
||||
}
|
||||
}, [lat, lon]);
|
||||
const router = useRouter();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
|
@ -43,11 +92,25 @@ export default function HomeScreen() {
|
|||
<View style={styles.rightContainer}>
|
||||
<Pressable
|
||||
style={styles.iconContainer}
|
||||
onPress={() => setIsSupportModalVisible(true)}
|
||||
onPress={() => {
|
||||
console.log("Support Pressed");
|
||||
setIsSupportModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<CustomerCareIcon />
|
||||
</Pressable>
|
||||
<Profile username="Vishal" textSize={16} boxSize={32} />
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
router.push("/user/profile");
|
||||
}}
|
||||
>
|
||||
<ProfileImage
|
||||
username="Vivek"
|
||||
onClick={() => router.push("/user/profile")}
|
||||
textSize={20}
|
||||
boxSize={40}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
|
|
@ -97,14 +160,14 @@ export default function HomeScreen() {
|
|||
<>
|
||||
<View style={styles.mapContainer}>
|
||||
<MapView
|
||||
provider={PROVIDER_GOOGLE}
|
||||
style={styles.mapStyle}
|
||||
initialRegion={{
|
||||
latitude: 28.54,
|
||||
longitude: 77.32,
|
||||
region={{
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
latitudeDelta: 0.0922,
|
||||
longitudeDelta: 0.0421,
|
||||
}}
|
||||
// customMapStyle={mapStyle}
|
||||
>
|
||||
<Marker
|
||||
draggable
|
||||
|
|
@ -112,13 +175,10 @@ export default function HomeScreen() {
|
|||
latitude: lat,
|
||||
longitude: lon,
|
||||
}}
|
||||
rotation={bearing}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
onDragEnd={(e) =>
|
||||
alert(JSON.stringify(e.nativeEvent.coordinate))
|
||||
}
|
||||
title={"Test Marker"}
|
||||
description={"This is a description of the marker"}
|
||||
image={require("../../assets/icons/marker.png")}
|
||||
tracksViewChanges={false}
|
||||
image={require("../../assets/images/marker.png")}
|
||||
/>
|
||||
</MapView>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
import EditScreenInfo from "@/components/EditScreenInfo";
|
||||
import { Text, View } from "@/components/Themed";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function MyBatteryTabScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Coming Soon</Text>
|
||||
<View
|
||||
style={styles.separator}
|
||||
lightColor="#eee"
|
||||
darkColor="rgba(255,255,255,0.1)"
|
||||
/>
|
||||
<EditScreenInfo path="app/(tabs)/two.tsx" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
import EditScreenInfo from "@/components/EditScreenInfo";
|
||||
import { Text, View } from "@/components/Themed";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function PaymentsTabScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Tab Two</Text>
|
||||
<View
|
||||
style={styles.separator}
|
||||
lightColor="#eee"
|
||||
darkColor="rgba(255,255,255,0.1)"
|
||||
/>
|
||||
<EditScreenInfo path="app/(tabs)/two.tsx" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
import EditScreenInfo from "@/components/EditScreenInfo";
|
||||
import { Text, View } from "@/components/Themed";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function ServiceTabScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Coming Soon</Text>
|
||||
<View
|
||||
style={styles.separator}
|
||||
lightColor="#eee"
|
||||
darkColor="rgba(255,255,255,0.1)"
|
||||
/>
|
||||
<EditScreenInfo path="app/(tabs)/two.tsx" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Link, Stack } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { Text, View } from '@/components/Themed';
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<Stack.Screen options={{ title: "Oops!" }} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>This screen doesn't exist.</Text>
|
||||
|
||||
|
|
@ -21,13 +21,13 @@ export default function NotFoundScreen() {
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: "bold",
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
|
|
@ -35,6 +35,6 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
color: '#2e78b7',
|
||||
color: "#2e78b7",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import { StatusBar } from 'expo-status-bar';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
||||
import { Text, View } from '@/components/Themed';
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Modal</Text>
|
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
||||
<EditScreenInfo path="app/modal.tsx" />
|
||||
|
||||
{/* Use a light status bar on iOS to account for the black space above the modal */}
|
||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
separator: {
|
||||
marginVertical: 30,
|
||||
height: 1,
|
||||
width: '80%',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { Stack } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import GoBack from "@/assets/icons/chevron_left.svg";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function AuthLayout() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "My Account",
|
||||
headerTitleStyle: {
|
||||
fontSize: 16,
|
||||
color: "#252A34",
|
||||
},
|
||||
headerShadowVisible: true,
|
||||
headerLeft: () => {
|
||||
return (
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<GoBack />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
animation: "slide_from_right",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<Image
|
||||
source={require("../../assets/images/user_image.jpg")}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<TouchableOpacity style={styles.editAvatar}>
|
||||
<MaterialIcons name="edit" size={20} color="#FDFDFD" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.textGroup}>
|
||||
<Text style={styles.label}>Name</Text>
|
||||
<Text style={styles.value}>Amar Kesari</Text>
|
||||
</View>
|
||||
<TouchableOpacity>
|
||||
<MaterialIcons name="edit" size={20} color="#555C70" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.divider} />
|
||||
<View style={styles.row}>
|
||||
<View style={styles.textGroup}>
|
||||
<Text style={styles.label}>Mobile Number</Text>
|
||||
<Text style={styles.value}>9876543210</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Other Menu Items */}
|
||||
<View style={styles.card}>
|
||||
{menuItem("My Vehicle")}
|
||||
<View style={styles.divider} />
|
||||
{menuItem("Language")}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
{menuItem("About App")}
|
||||
<View style={styles.divider} />
|
||||
{menuItem("Logout")}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const menuItem = (title: string) => (
|
||||
<TouchableOpacity style={styles.menuRow}>
|
||||
<Text style={styles.menuText}>{title}</Text>
|
||||
<MaterialIcons name="chevron-right" size={20} color="#555C70" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderWidth: 1,
|
||||
flex: 1,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginLeft: 8,
|
||||
color: "#252A34",
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
avatarContainer: {
|
||||
alignItems: "center",
|
||||
marginVertical: 24,
|
||||
},
|
||||
avatar: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
},
|
||||
editAvatar: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 105,
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: "#008866",
|
||||
borderRadius: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
card: {
|
||||
backgroundColor: "#FCFCFC",
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
},
|
||||
textGroup: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
color: "#252A34",
|
||||
},
|
||||
value: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#252A34",
|
||||
marginTop: 2,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: "#E5E9F0",
|
||||
marginVertical: 4,
|
||||
},
|
||||
menuRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: 16,
|
||||
},
|
||||
menuText: {
|
||||
fontSize: 14,
|
||||
color: "#252A34",
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 707 KiB |
|
|
@ -1,83 +0,0 @@
|
|||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { ExternalLink } from "./ExternalLink";
|
||||
import { MonoText } from "./StyledText";
|
||||
import { Text, View } from "./Themed";
|
||||
|
||||
import Colors from "@/constants/Colors";
|
||||
|
||||
export default function EditScreenInfo({ path }: { path: string }) {
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.getStartedContainer}>
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)"
|
||||
>
|
||||
Open up the code for this screen:
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
|
||||
darkColor="rgba(255,255,255,0.05)"
|
||||
lightColor="rgba(0,0,0,0.05)"
|
||||
>
|
||||
<MonoText>{path}</MonoText>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)"
|
||||
>
|
||||
Change any of the text, save the file, and your app will automatically
|
||||
update.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpContainer}>
|
||||
<ExternalLink
|
||||
style={styles.helpLink}
|
||||
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet"
|
||||
>
|
||||
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
|
||||
Tap here if your app doesn't automatically update after making
|
||||
changes
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
getStartedContainer: {
|
||||
alignItems: "center",
|
||||
marginHorizontal: 50,
|
||||
},
|
||||
homeScreenFilename: {
|
||||
marginVertical: 7,
|
||||
},
|
||||
codeHighlightContainer: {
|
||||
borderRadius: 3,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
getStartedText: {
|
||||
fontSize: 17,
|
||||
lineHeight: 24,
|
||||
textAlign: "center",
|
||||
},
|
||||
helpContainer: {
|
||||
marginTop: 15,
|
||||
marginHorizontal: 20,
|
||||
alignItems: "center",
|
||||
},
|
||||
helpLink: {
|
||||
paddingVertical: 15,
|
||||
},
|
||||
helpLinkText: {
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"build": {
|
||||
"development": {
|
||||
"env": {
|
||||
"ENV": "development",
|
||||
"BASE_URL": "https://dev-api-service.vecmocon.com/service-buddy"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"env": {
|
||||
"ENV": "production",
|
||||
"BASE_URL": "https://dev-api-service.vecmocon.com/service-buddy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import { BmsState } from "@/constants/types";
|
|||
const SERVER_URL =
|
||||
"http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504";
|
||||
const TOKEN =
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE0NTgxNDQsImV4cCI6MTc1MTU0NDU0NH0.ETM2hCU_5EtVcKAABd_69fyxS-FNuJ5Mv-QbY014sBY";
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE1Mzg0MDAsImV4cCI6MTc1MTYyNDgwMH0.QIGyV9_jbtv0F8YzbzIgn_669HJz2ftI8KckpPGN0UU";
|
||||
|
||||
let socket: Socket | null = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { BmsState } from "@/constants/types";
|
||||
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
|
||||
interface TelemetryState {
|
||||
SoH: number;
|
||||
SoC: number;
|
||||
SoH: number | null;
|
||||
SoC: number | null;
|
||||
chargingState: BmsState | null;
|
||||
lat: number | null;
|
||||
lon: number | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: TelemetryState = {
|
||||
SoH: 0,
|
||||
SoC: 0,
|
||||
SoH: null,
|
||||
SoC: null,
|
||||
chargingState: null,
|
||||
lat: null,
|
||||
lon: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const telemetrySlice = createSlice({
|
||||
|
|
@ -17,8 +28,17 @@ export const telemetrySlice = createSlice({
|
|||
updateTelemetry: (state, action: PayloadAction<TelemetryState>) => {
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
setTelemetryLoading: (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
},
|
||||
setTelemetryError: (state, action: PayloadAction<string>) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateTelemetry } = telemetrySlice.actions;
|
||||
export const { updateTelemetry, setTelemetryLoading, setTelemetryError } =
|
||||
telemetrySlice.actions;
|
||||
export default telemetrySlice.reducer;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { AppTheme } from "./types";
|
||||
|
||||
export const DarkTheme: AppTheme = {
|
||||
dark: true,
|
||||
colors: {
|
||||
primary: "#008761",
|
||||
background: "#252A34",
|
||||
card: "#1C1E21",
|
||||
text: "#F3F5F8",
|
||||
border: "#4A4F57",
|
||||
notification: "#F4C5C3",
|
||||
|
||||
secondaryBackground: "#1C1E21",
|
||||
accent: "#F3F5F8",
|
||||
success: "#B8F1E3",
|
||||
warning: "#FDE9E7",
|
||||
info: "#D7E6FD",
|
||||
muted: "#B9BDC5",
|
||||
},
|
||||
fonts: {
|
||||
regular: "System",
|
||||
bold: "System",
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { LightTheme } from "./light";
|
||||
export { DarkTheme } from "./dark";
|
||||
export type { AppTheme } from "./types";
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { AppTheme } from "./types";
|
||||
|
||||
export const LightTheme: AppTheme = {
|
||||
dark: false,
|
||||
colors: {
|
||||
primary: "#008761",
|
||||
background: "#F3F5F8",
|
||||
card: "#FFFFFF",
|
||||
text: "#1C1E21",
|
||||
border: "#B9BDC5",
|
||||
notification: "#F4C5C3",
|
||||
|
||||
// custom additions
|
||||
secondaryBackground: "#E5EBFD",
|
||||
accent: "#252A34",
|
||||
success: "#B8F1E3",
|
||||
warning: "#FDE9E7",
|
||||
info: "#D7E6FD",
|
||||
muted: "#4A4F57",
|
||||
},
|
||||
fonts: {
|
||||
regular: "System",
|
||||
bold: "System",
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export interface AppTheme {
|
||||
dark: boolean;
|
||||
colors: {
|
||||
primary: string;
|
||||
background: string;
|
||||
card: string;
|
||||
text: string;
|
||||
border: string;
|
||||
notification: string;
|
||||
|
||||
secondaryBackground?: string;
|
||||
accent?: string;
|
||||
success?: string;
|
||||
warning?: string;
|
||||
info?: string;
|
||||
muted?: string;
|
||||
};
|
||||
fonts: {
|
||||
regular: string;
|
||||
bold?: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { useTheme } from "@react-navigation/native";
|
||||
import { AppTheme } from "./types";
|
||||
|
||||
export function useThemeColors(): AppTheme["colors"] {
|
||||
const { colors } = useTheme();
|
||||
return colors;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
export function calculateBearing(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number
|
||||
): number {
|
||||
const φ1 = (lat1 * Math.PI) / 180; // Convert to radians
|
||||
const φ2 = (lat2 * Math.PI) / 180;
|
||||
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
||||
|
||||
// Bearing formula
|
||||
const y = Math.sin(Δλ) * Math.cos(φ2);
|
||||
const x =
|
||||
Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
||||
let θ = Math.atan2(y, x);
|
||||
θ = (θ * 180) / Math.PI; // Convert back to degrees
|
||||
return (θ + 360) % 360; // Normalize to 0-360°
|
||||
}
|
||||
|
||||
export function calculateDistance(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number
|
||||
): number {
|
||||
const R = 6371e3; // Earth 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
|
||||
}
|
||||
Loading…
Reference in New Issue