From 4168f1a4b79f61dc702b74a47bcc9c38cec375d3 Mon Sep 17 00:00:00 2001 From: vinay kumar Date: Mon, 30 Jun 2025 21:55:53 +0530 Subject: [PATCH] Add home screen components --- app/auth/verify.tsx | 0 assets/icons/call.svg | 8 ++ assets/icons/chevron_rightside.svg | 8 ++ assets/icons/close.svg | 8 ++ assets/icons/customer-care.svg | 8 ++ assets/icons/danger.svg | 8 ++ assets/icons/error.svg | 8 ++ assets/icons/mail.svg | 8 ++ assets/icons/warning.svg | 8 ++ assets/icons/whatsapp.svg | 3 + components/EditScreenInfo.tsx | 38 +++--- components/home/BatteryWarrantyCars.tsx | 112 +++++++++++++++++ components/home/CustomerSupportModal.tsx | 151 +++++++++++++++++++++++ components/home/LocationMap.tsx | 39 ++++++ components/home/MetricCard.tsx | 48 +++++++ components/home/PaymentDueCard.tsx | 95 ++++++++++++++ components/home/Profile.tsx | 45 +++++++ components/home/SemiCircleProgress.tsx | 147 ++++++++++++++++++++++ components/home/ServiceReminderCard.tsx | 82 ++++++++++++ constants/config.ts | 56 ++++++++- 20 files changed, 861 insertions(+), 19 deletions(-) create mode 100644 app/auth/verify.tsx create mode 100644 assets/icons/call.svg create mode 100644 assets/icons/chevron_rightside.svg create mode 100644 assets/icons/close.svg create mode 100644 assets/icons/customer-care.svg create mode 100644 assets/icons/danger.svg create mode 100644 assets/icons/error.svg create mode 100644 assets/icons/mail.svg create mode 100644 assets/icons/warning.svg create mode 100644 assets/icons/whatsapp.svg create mode 100644 components/home/BatteryWarrantyCars.tsx create mode 100644 components/home/CustomerSupportModal.tsx create mode 100644 components/home/LocationMap.tsx create mode 100644 components/home/MetricCard.tsx create mode 100644 components/home/PaymentDueCard.tsx create mode 100644 components/home/Profile.tsx create mode 100644 components/home/SemiCircleProgress.tsx create mode 100644 components/home/ServiceReminderCard.tsx diff --git a/app/auth/verify.tsx b/app/auth/verify.tsx new file mode 100644 index 0000000..e69de29 diff --git a/assets/icons/call.svg b/assets/icons/call.svg new file mode 100644 index 0000000..ddd4155 --- /dev/null +++ b/assets/icons/call.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/chevron_rightside.svg b/assets/icons/chevron_rightside.svg new file mode 100644 index 0000000..04994b9 --- /dev/null +++ b/assets/icons/chevron_rightside.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/close.svg b/assets/icons/close.svg new file mode 100644 index 0000000..2359b61 --- /dev/null +++ b/assets/icons/close.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/customer-care.svg b/assets/icons/customer-care.svg new file mode 100644 index 0000000..6370dfa --- /dev/null +++ b/assets/icons/customer-care.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/danger.svg b/assets/icons/danger.svg new file mode 100644 index 0000000..4eaa686 --- /dev/null +++ b/assets/icons/danger.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/error.svg b/assets/icons/error.svg new file mode 100644 index 0000000..bdbaedb --- /dev/null +++ b/assets/icons/error.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/mail.svg b/assets/icons/mail.svg new file mode 100644 index 0000000..2ab3939 --- /dev/null +++ b/assets/icons/mail.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg new file mode 100644 index 0000000..019a3c1 --- /dev/null +++ b/assets/icons/warning.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/whatsapp.svg b/assets/icons/whatsapp.svg new file mode 100644 index 0000000..6c24ce6 --- /dev/null +++ b/assets/icons/whatsapp.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/EditScreenInfo.tsx b/components/EditScreenInfo.tsx index 430b609..9b4a437 100644 --- a/components/EditScreenInfo.tsx +++ b/components/EditScreenInfo.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import { StyleSheet } from 'react-native'; +import React from "react"; +import { StyleSheet } from "react-native"; -import { ExternalLink } from './ExternalLink'; -import { MonoText } from './StyledText'; -import { Text, View } from './Themed'; +import { ExternalLink } from "./ExternalLink"; +import { MonoText } from "./StyledText"; +import { Text, View } from "./Themed"; -import Colors from '@/constants/Colors'; +import Colors from "@/constants/Colors"; export default function EditScreenInfo({ path }: { path: string }) { return ( @@ -14,31 +14,37 @@ export default function EditScreenInfo({ path }: { path: string }) { + darkColor="rgba(255,255,255,0.8)" + > Open up the code for this screen: + lightColor="rgba(0,0,0,0.05)" + > {path} - Change any of the text, save the file, and your app will automatically update. + darkColor="rgba(255,255,255,0.8)" + > + Change any of the text, save the file, and your app will automatically + update. + href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet" + > - Tap here if your app doesn't automatically update after making changes + Tap here if your app doesn't automatically update after making + changes @@ -48,7 +54,7 @@ export default function EditScreenInfo({ path }: { path: string }) { const styles = StyleSheet.create({ getStartedContainer: { - alignItems: 'center', + alignItems: "center", marginHorizontal: 50, }, homeScreenFilename: { @@ -61,17 +67,17 @@ const styles = StyleSheet.create({ getStartedText: { fontSize: 17, lineHeight: 24, - textAlign: 'center', + textAlign: "center", }, helpContainer: { marginTop: 15, marginHorizontal: 20, - alignItems: 'center', + alignItems: "center", }, helpLink: { paddingVertical: 15, }, helpLinkText: { - textAlign: 'center', + textAlign: "center", }, }); diff --git a/components/home/BatteryWarrantyCars.tsx b/components/home/BatteryWarrantyCars.tsx new file mode 100644 index 0000000..501f330 --- /dev/null +++ b/components/home/BatteryWarrantyCars.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { View, Text, StyleSheet, Pressable } from "react-native"; +import ChevronRight from "../../assets/icons/chevron_rightside.svg"; + +type Props = { + totalWarrantyYears: number; // in years + batteryPurchaseEpoch: number; // in seconds +}; + +const BatteryWarrantyCard: React.FC = ({ + totalWarrantyYears, + batteryPurchaseEpoch, +}) => { + const totalWarrantyMs = totalWarrantyYears * 365.25 * 24 * 60 * 60 * 1000; + const purchaseDate = new Date(batteryPurchaseEpoch * 1000); + const now = new Date(); + const elapsed = now.getTime() - purchaseDate.getTime(); + const remaining = Math.max(totalWarrantyMs - elapsed, 0); + const progress = Math.min(elapsed / totalWarrantyMs, 1); + + const remainingDate = new Date(remaining); + const yearsLeft = Math.floor(remaining / (365.25 * 24 * 60 * 60 * 1000)); + const monthsLeft = Math.floor( + (remaining % (365.25 * 24 * 60 * 60 * 1000)) / (30.44 * 24 * 60 * 60 * 1000) + ); + const daysLeft = Math.floor( + (remaining % (30.44 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000) + ); + + return ( + + + + Battery Warranty Left + + {`${yearsLeft} years, ${monthsLeft} month${ + monthsLeft !== 1 ? "s" : "" + }, ${daysLeft} day${daysLeft !== 1 ? "s" : ""}`} + + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: "#FCFCFC", + borderRadius: 10, + padding: 16, + width: "100%", + height: 96, + justifyContent: "space-between", + }, + topRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + }, + textColumn: { + flexDirection: "column", + gap: 4, + }, + title: { + fontSize: 12, + fontFamily: "Inter-Medium", + color: "#565E70", + lineHeight: 16, + }, + time: { + fontSize: 14, + fontFamily: "Inter-SemiBold", + color: "#252A34", + lineHeight: 20, + }, + iconButton: { + width: 40, + height: 40, + padding: 10, + justifyContent: "center", + alignItems: "center", + }, + progressContainer: { + marginTop: 8, + width: "100%", + height: 8, + }, + progressBar: { + width: "100%", + height: 8, + backgroundColor: "#E5E9F0", + borderRadius: 10, + overflow: "hidden", + }, + progressFill: { + height: "100%", + backgroundColor: "#027A48", + borderRadius: 10, + }, +}); + +export default BatteryWarrantyCard; diff --git a/components/home/CustomerSupportModal.tsx b/components/home/CustomerSupportModal.tsx new file mode 100644 index 0000000..4ecb20a --- /dev/null +++ b/components/home/CustomerSupportModal.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import { + View, + Text, + StyleSheet, + Pressable, + Linking, + Modal, +} from "react-native"; +import WhatsappIcon from "../../assets/icons/whatsapp.svg"; +import CallIcon from "../../assets/icons/call.svg"; +import EmailIcon from "../../assets/icons/mail.svg"; +import CloseIcon from "../../assets/icons/close.svg"; + +import { SUPPORT } from "@/constants/config"; +import { StatusBar } from "expo-status-bar"; + +type Props = { + visible: boolean; + onClose: () => void; +}; + +const CustomerSupportModal: React.FC = ({ visible, onClose }) => { + const openWhatsApp = async () => { + const url = `https://wa.me/${ + SUPPORT.WHATSAPP_NUMBER + }?text=${encodeURIComponent(SUPPORT.WHATSAPP_PLACEHOLDER)}`; + + Linking.openURL(url).catch((err) => + console.log("Failed to open WhatsApp:", err) + ); + }; + + const makePhoneCall = () => { + Linking.openURL(`tel:${SUPPORT.PHONE}`); + }; + + const sendEmail = () => { + const url = `mailto:${SUPPORT.EMAIL}?subject=${encodeURIComponent( + SUPPORT.EMAIL_SUBJECT + )}&body=${encodeURIComponent(SUPPORT.EMAIL_BODY)}`; + Linking.openURL(url); + }; + + return ( + + + + + {/* Header */} + + Customer Support + + + + + + {/* Buttons */} + + + + + Whatsapp + + + + Call Us + + + + + Email + + + + + + ); +}; + +export default CustomerSupportModal; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: "flex-end", + backgroundColor: "rgba(0,0,0,0.5)", + }, + modalContainer: { + backgroundColor: "#fff", + borderTopLeftRadius: 12, + borderTopRightRadius: 12, + paddingHorizontal: 16, + paddingVertical: 16, + }, + header: { + height: 56, + borderBottomWidth: 1, + borderBottomColor: "#E5E9F0", + paddingHorizontal: 16, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + headerText: { + fontSize: 14, + fontWeight: "600", + color: "#252A34", + }, + iconButton: { + width: 40, + height: 40, + padding: 10, + justifyContent: "center", + alignItems: "center", + }, + content: { + paddingVertical: 16, + gap: 16, + }, + row: { + flexDirection: "row", + gap: 16, + justifyContent: "space-between", + }, + secondaryButton: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingVertical: 8, + paddingHorizontal: 16, + height: 40, + borderRadius: 4, + backgroundColor: "#F3F5F8", + borderWidth: 1, + borderColor: "#D8DDE7", + width: 156, + gap: 8, + }, + fullButton: { + width: "100%", + }, + buttonText: { + fontSize: 14, + fontWeight: "500", + color: "#252A34", + }, +}); diff --git a/components/home/LocationMap.tsx b/components/home/LocationMap.tsx new file mode 100644 index 0000000..53839fc --- /dev/null +++ b/components/home/LocationMap.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { View, StyleSheet } from "react-native"; +import MapView, { Marker } from "react-native-maps"; + +interface LocationMapProps { + latitude: number; + longitude: number; +} + +const LocationMap: React.FC = ({ latitude, longitude }) => { + const region = { + latitude, + longitude, + latitudeDelta: 0.01, + longitudeDelta: 0.01, + }; + + return ( + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + borderRadius: 10, + overflow: "hidden", + }, + map: { + width: "100%", + height: "100%", + }, +}); + +export default LocationMap; diff --git a/components/home/MetricCard.tsx b/components/home/MetricCard.tsx new file mode 100644 index 0000000..efe064d --- /dev/null +++ b/components/home/MetricCard.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { View, Text, StyleSheet } from "react-native"; + +interface MetricCardProps { + heading: string; + value: string | number; +} + +const MetricCard: React.FC = ({ heading, value }) => { + return ( + + + {heading} + {value} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + width: 156, + height: 68, + backgroundColor: "#FCFCFC", + borderRadius: 8, + padding: 12, + justifyContent: "center", + }, + textContainer: { + flexDirection: "column", + justifyContent: "flex-start", + gap: 8, + }, + heading: { + fontSize: 12, + lineHeight: 16, + color: "#565F70", + fontFamily: "Inter-Medium", + }, + value: { + fontSize: 14, + lineHeight: 20, + color: "#252A34", + fontFamily: "Inter-SemiBold", + }, +}); + +export default MetricCard; diff --git a/components/home/PaymentDueCard.tsx b/components/home/PaymentDueCard.tsx new file mode 100644 index 0000000..f9baa01 --- /dev/null +++ b/components/home/PaymentDueCard.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Dimensions, +} from "react-native"; +import { MaterialIcons } from "@expo/vector-icons"; + +interface PaymentDueCardProps { + label: string; + amount: string; + onPress: () => void; +} + +const PaymentDueCard: React.FC = ({ + label, + amount, + onPress, +}) => { + return ( + + + + + + + {label} + {amount} + + + + Pay Now + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: "#FCFCFC", + borderRadius: 10, + padding: 16, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + gap: 16, + }, + leftSection: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + iconWrapper: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "#FFEAEA", + justifyContent: "center", + alignItems: "center", + }, + textGroup: { + marginLeft: 12, + }, + label: { + fontSize: 12, + color: "#565F70", + fontFamily: "Inter-Medium", + marginBottom: 4, + }, + amount: { + fontSize: 14, + color: "#252A34", + fontFamily: "Inter-SemiBold", + }, + button: { + backgroundColor: "#00825B", + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 4, + }, + buttonText: { + color: "#FCFCFC", + fontSize: 12, + fontFamily: "Inter-Medium", + }, +}); + +export default PaymentDueCard; diff --git a/components/home/Profile.tsx b/components/home/Profile.tsx new file mode 100644 index 0000000..dded6dd --- /dev/null +++ b/components/home/Profile.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { Text, TouchableOpacity, StyleSheet } from "react-native"; + +type ProfileImageProps = { + username: string; + onClick?: () => void; + textSize?: number; + boxSize?: number; +}; + +const ProfileImage: React.FC = ({ + username, + onClick, + textSize, + boxSize, +}) => { + const firstLetter = username?.substring(0, 1)?.toUpperCase() || "V"; + + return ( + + + {firstLetter} + + + ); +}; + +export default ProfileImage; + +const styles = StyleSheet.create({ + container: { + borderRadius: 394.316, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#DAF5ED", + }, + letter: { + color: "#008761", + fontFamily: "Inter", + fontWeight: "bold", + }, +}); diff --git a/components/home/SemiCircleProgress.tsx b/components/home/SemiCircleProgress.tsx new file mode 100644 index 0000000..0eea948 --- /dev/null +++ b/components/home/SemiCircleProgress.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useRef, useState } from "react"; +import { View, StyleSheet, Text, Animated } from "react-native"; +import Svg, { Circle, Defs, LinearGradient, Stop } from "react-native-svg"; + +const AnimatedCircle = Animated.createAnimatedComponent(Circle); + +const CircleProgressBar = ({ + progress, + status, +}: { + progress: number; + status: number; +}) => { + const radius = 20; + const strokeWidth = 5; + const viewBoxPadding = 4; + const viewBoxSize = (radius + strokeWidth) * 2 + viewBoxPadding; + const center = viewBoxSize / 2; + const circumference = 2 * Math.PI * radius; + + const animatedValue = useRef(new Animated.Value(0)).current; + const [displayProgress, setDisplayProgress] = useState(0); + + useEffect(() => { + if (progress === undefined) { + animatedValue.setValue(0); + setDisplayProgress(0); + } else { + Animated.timing(animatedValue, { + toValue: progress, + duration: 500, + useNativeDriver: false, + }).start(); + } + }, [progress]); + + useEffect(() => { + const listenerId = animatedValue.addListener(({ value }) => { + setDisplayProgress(Math.round(value)); + }); + return () => animatedValue.removeListener(listenerId); + }, []); + + const getColor = (progress: number) => { + if (progress <= 20) return "#D51D10"; + if (progress <= 50) return "#FF7B00"; + return "#009E71"; + }; + + const animatedColor = getColor(displayProgress); + + const dashOffset = animatedValue.interpolate({ + inputRange: [0, 100], + outputRange: [circumference, circumference - 0.7 * circumference], + }); + + const progressDashArray = `${circumference} ${circumference}`; + const backgroundDashArray = `${0.7 * circumference} ${0.3 * circumference}`; + + return ( + + + + + + + + + + + + + + {progress === undefined ? "---" : displayProgress} + + + {progress === undefined ? null : "%"} + + + + + {status === 1 ? "Charging" : "Discharging"} + + + + ); +}; + +// Keep the styles the same as before + +const styles = StyleSheet.create({ + container: { + position: "relative", + alignItems: "center", + }, + circleContainer: { + transform: [{ rotate: "144deg" }], + }, + batteryPercent: { + position: "absolute", + top: 80, + justifyContent: "center", + alignItems: "baseline", + display: "flex", + flexDirection: "row", + }, + batterySoc: { + position: "absolute", + top: 210, + justifyContent: "center", + transform: [{ translateX: 0 }], + }, + batteryStatus: { + position: "absolute", + top: 300, + justifyContent: "center", + transform: [{ translateX: 0 }], + }, +}); + +export default CircleProgressBar; diff --git a/components/home/ServiceReminderCard.tsx b/components/home/ServiceReminderCard.tsx new file mode 100644 index 0000000..aed268c --- /dev/null +++ b/components/home/ServiceReminderCard.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ViewStyle, +} from "react-native"; +import Feather from "../../assets/icons/chevron_rightside.svg"; +import { ALERT_STYLES } from "@/constants/config"; + +type AlertType = "info" | "warning" | "danger"; + +interface AlertCardProps { + type: AlertType; + message: string; + subMessage?: string; +} + +const AlertCard: React.FC = ({ type, message, subMessage }) => { + const style = ALERT_STYLES[type]; + + const containerStyle: ViewStyle[] = [ + styles.container, + { backgroundColor: style.backgroundColor }, + ]; + return ( + + + + + + {message} + {subMessage ? `\n${subMessage}` : ""} + + + + + + ); +}; + +const styles = StyleSheet.create({ + leftContent: { + flexDirection: "row", + gap: 8, + }, + container: { + flexDirection: "row", + backgroundColor: "#E5EBFD", + borderRadius: 8, + padding: 8, + width: "100%", + alignSelf: "center", + alignItems: "flex-start", + justifyContent: "space-between", + height: 60, + marginHorizontal: 16, + }, + content: { + flexDirection: "row", + gap: 8, + }, + textContainer: { + marginLeft: 8, + justifyContent: "center", + }, + text: { + color: "#252A34", + fontSize: 14, + lineHeight: 20, + fontFamily: "Inter-Regular", + }, + boldText: { + fontWeight: "500", + }, +}); + +export default AlertCard; diff --git a/constants/config.ts b/constants/config.ts index 3072a9b..8082c3a 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -6,11 +6,21 @@ import ServiceIcon from "../assets/icons/service.svg"; import ServiceIconFilled from "../assets/icons/service-filled.svg"; import BatteryIcon from "../assets/icons/battery.svg"; import BatteryIconFilled from "../assets/icons/battery-filled.svg"; +import { BASE_URL, ENV } from "@env"; +import InfoIcon from "../assets/icons/error.svg"; +import WarningIcon from "../assets/icons/warning.svg"; +import DangerIcon from "../assets/icons/danger.svg"; + +export default { + ENV, + BASE_URL, +}; export const STORAGE_KEYS = { - LANGUAGE: "USER_LANGUAGE", - TOKEN: "AUTH_TOKEN", - THEME: "APP_THEME", + LANGUAGE: "userLanguage", + AUTH_TOKEN: "authToken", + THEME: "appTheme", + REFRESH_TOKEN: "refreshToken", }; export const APP_CONFIG = { @@ -48,3 +58,43 @@ export const TAB_CONFIG = [ path: "/my-battery", }, ]; + +export const MESSAGES = { + AUTHENTICATION: { + INVALID_TOKEN: "Invalid Token", + VERIFICATION_FAILED: "Verification failed, try again later", + }, +}; + +export const AUTH_STATUSES = { + IDLE: "Idle", + LOADING: "Loading", + SUCCESS: "Success", + FAILED: "Failed", +} as const; + +export const ALERT_STYLES = { + info: { + backgroundColor: "#E5EBFD", + icon: InfoIcon, + }, + warning: { + backgroundColor: "#FFF2E2", + icon: WarningIcon, + }, + danger: { + backgroundColor: "#FFE9E9", + icon: DangerIcon, + }, +}; + +export const SUPPORT = { + WHATSAPP_NUMBER: "918685846459", + WHATSAPP_PLACEHOLDER: "Hi, I need help regarding my vehicle.", + PHONE: "+911234567890", + EMAIL: "support@vecmocon.com", + EMAIL_SUBJECT: "Support Request", + EMAIL_BODY: "Hello,\n\nI need assistance with...", +}; + +export type StatusType = (typeof AUTH_STATUSES)[keyof typeof AUTH_STATUSES];