Add profile screen and fix map's motion

feature/app-setup
vinay kumar 2025-07-03 17:42:56 +05:30
parent 6db5b1355b
commit 75d43a3cd9
21 changed files with 453 additions and 176 deletions

View File

@ -6,7 +6,7 @@
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "batteryasaservice", "scheme": "batteryasaservice",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "light",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": {
"image": "./assets/images/splash-icon.png", "image": "./assets/images/splash-icon.png",

View File

@ -6,22 +6,24 @@ import {
} from "react-native"; } from "react-native";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SemiCircleProgress from "../../components/home/SemiCircleProgress"; import SemiCircleProgress from "../../components/home/SemiCircleProgress";
import { Text, View } from "@/components/Themed"; import { Text, View } from "react-native";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import { useLayoutEffect, useState } from "react"; import { useEffect, useLayoutEffect, useState } from "react";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import Profile from "../../components/home/Profile";
import CustomerCareIcon from "../../assets/icons/customer-care.svg"; import CustomerCareIcon from "../../assets/icons/customer-care.svg";
import ServiceReminderCard from "@/components/home/ServiceReminderCard"; import ServiceReminderCard from "@/components/home/ServiceReminderCard";
import MetricCard from "@/components/home/MetricCard"; import MetricCard from "@/components/home/MetricCard";
import PaymentDueCard from "@/components/home/PaymentDueCard"; 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 BatteryWarrantyCard from "@/components/home/BatteryWarrantyCars";
import CustomerSupportModal from "@/components/home/CustomerSupportModal"; import CustomerSupportModal from "@/components/home/CustomerSupportModal";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "@/store"; import { RootState } from "@/store";
import LocationOff from "@/assets/icons/location_off.svg"; import LocationOff from "@/assets/icons/location_off.svg";
import { Linking } from "react-native"; 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() { export default function HomeScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -30,6 +32,53 @@ export default function HomeScreen() {
const { SoC, SoH, chargingState, lat, lon, loading, error } = useSelector( const { SoC, SoH, chargingState, lat, lon, loading, error } = useSelector(
(state: RootState) => state.telemetry (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(() => { useLayoutEffect(() => {
navigation.setOptions({ navigation.setOptions({
@ -43,11 +92,25 @@ export default function HomeScreen() {
<View style={styles.rightContainer}> <View style={styles.rightContainer}>
<Pressable <Pressable
style={styles.iconContainer} style={styles.iconContainer}
onPress={() => setIsSupportModalVisible(true)} onPress={() => {
console.log("Support Pressed");
setIsSupportModalVisible(true);
}}
> >
<CustomerCareIcon /> <CustomerCareIcon />
</Pressable> </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> </View>
), ),
}); });
@ -97,14 +160,14 @@ export default function HomeScreen() {
<> <>
<View style={styles.mapContainer}> <View style={styles.mapContainer}>
<MapView <MapView
provider={PROVIDER_GOOGLE}
style={styles.mapStyle} style={styles.mapStyle}
initialRegion={{ region={{
latitude: 28.54, latitude: lat,
longitude: 77.32, longitude: lon,
latitudeDelta: 0.0922, latitudeDelta: 0.0922,
longitudeDelta: 0.0421, longitudeDelta: 0.0421,
}} }}
// customMapStyle={mapStyle}
> >
<Marker <Marker
draggable draggable
@ -112,13 +175,10 @@ export default function HomeScreen() {
latitude: lat, latitude: lat,
longitude: lon, longitude: lon,
}} }}
rotation={bearing}
anchor={{ x: 0.5, y: 0.5 }} anchor={{ x: 0.5, y: 0.5 }}
onDragEnd={(e) => tracksViewChanges={false}
alert(JSON.stringify(e.nativeEvent.coordinate)) image={require("../../assets/images/marker.png")}
}
title={"Test Marker"}
description={"This is a description of the marker"}
image={require("../../assets/icons/marker.png")}
/> />
</MapView> </MapView>
</View> </View>

View File

@ -1,18 +1,11 @@
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import EditScreenInfo from "@/components/EditScreenInfo"; import { Text, View } from "react-native";
import { Text, View } from "@/components/Themed";
export default function MyBatteryTabScreen() { export default function MyBatteryTabScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Coming Soon</Text> <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> </View>
); );
} }

View File

@ -1,18 +1,10 @@
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import { Text, View } from "react-native";
import EditScreenInfo from "@/components/EditScreenInfo";
import { Text, View } from "@/components/Themed";
export default function PaymentsTabScreen() { export default function PaymentsTabScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Tab Two</Text> <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> </View>
); );
} }

View File

@ -1,18 +1,10 @@
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import { Text, View } from "react-native";
import EditScreenInfo from "@/components/EditScreenInfo";
import { Text, View } from "@/components/Themed";
export default function ServiceTabScreen() { export default function ServiceTabScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Coming Soon</Text> <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> </View>
); );
} }

View File

@ -1,12 +1,12 @@
import { Link, Stack } from 'expo-router'; import { Link, Stack } from "expo-router";
import { StyleSheet } from 'react-native'; import { StyleSheet } from "react-native";
import { Text, View } from '@/components/Themed'; import { Text, View } from "react-native";
export default function NotFoundScreen() { export default function NotFoundScreen() {
return ( return (
<> <>
<Stack.Screen options={{ title: 'Oops!' }} /> <Stack.Screen options={{ title: "Oops!" }} />
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text> <Text style={styles.title}>This screen doesn't exist.</Text>
@ -21,13 +21,13 @@ export default function NotFoundScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
padding: 20, padding: 20,
}, },
title: { title: {
fontSize: 20, fontSize: 20,
fontWeight: 'bold', fontWeight: "bold",
}, },
link: { link: {
marginTop: 15, marginTop: 15,
@ -35,6 +35,6 @@ const styles = StyleSheet.create({
}, },
linkText: { linkText: {
fontSize: 14, fontSize: 14,
color: '#2e78b7', color: "#2e78b7",
}, },
}); });

View File

@ -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%',
},
});

51
app/user/_layout.tsx Normal file
View File

@ -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",
},
});

153
app/user/profile.tsx Normal file
View File

@ -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",
},
});

BIN
assets/images/marker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

View File

@ -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",
},
});

16
eas.json Normal file
View File

@ -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"
}
}
}
}

View File

@ -10,7 +10,7 @@ import { BmsState } from "@/constants/types";
const SERVER_URL = const SERVER_URL =
"http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504"; "http://dev.vec-tr.ai:8089/?dashboardId=deviceDashboardSocket&assetId=V16000868651064644504";
const TOKEN = const TOKEN =
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE0NTgxNDQsImV4cCI6MTc1MTU0NDU0NH0.ETM2hCU_5EtVcKAABd_69fyxS-FNuJ5Mv-QbY014sBY"; "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2NCIsImFjdGlvbiI6ImF1dGgiLCJ0b2tlbi12ZXJzaW9uIjowLCJpYXQiOjE3NTE1Mzg0MDAsImV4cCI6MTc1MTYyNDgwMH0.QIGyV9_jbtv0F8YzbzIgn_669HJz2ftI8KckpPGN0UU";
let socket: Socket | null = null; let socket: Socket | null = null;

View File

@ -1,13 +1,24 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { BmsState } from "@/constants/types";
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
interface TelemetryState { interface TelemetryState {
SoH: number; SoH: number | null;
SoC: number; SoC: number | null;
chargingState: BmsState | null;
lat: number | null;
lon: number | null;
loading: boolean;
error: string | null;
} }
const initialState: TelemetryState = { const initialState: TelemetryState = {
SoH: 0, SoH: null,
SoC: 0, SoC: null,
chargingState: null,
lat: null,
lon: null,
loading: false,
error: null,
}; };
export const telemetrySlice = createSlice({ export const telemetrySlice = createSlice({
@ -17,8 +28,17 @@ export const telemetrySlice = createSlice({
updateTelemetry: (state, action: PayloadAction<TelemetryState>) => { updateTelemetry: (state, action: PayloadAction<TelemetryState>) => {
return { ...state, ...action.payload }; 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; export default telemetrySlice.reducer;

24
theme/dark.ts Normal file
View File

@ -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",
},
};

3
theme/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { LightTheme } from "./light";
export { DarkTheme } from "./dark";
export type { AppTheme } from "./types";

25
theme/light.ts Normal file
View File

@ -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",
},
};

22
theme/types.ts Normal file
View File

@ -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;
};
}

7
theme/useThemeColors.ts Normal file
View File

@ -0,0 +1,7 @@
import { useTheme } from "@react-navigation/native";
import { AppTheme } from "./types";
export function useThemeColors(): AppTheme["colors"] {
const { colors } = useTheme();
return colors;
}

37
utils/Map.ts Normal file
View File

@ -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
}