diff --git a/apps/delivery/.env.example b/apps/delivery/.env.example new file mode 100644 index 00000000..df0aa30b --- /dev/null +++ b/apps/delivery/.env.example @@ -0,0 +1,2 @@ +EXPO_PUBLIC_API_URL="" +EXPO_PUBLIC_GOOGLE_MAPS_APIKEY="" diff --git a/apps/delivery/.gitignore b/apps/delivery/.gitignore new file mode 100644 index 00000000..0b37b6eb --- /dev/null +++ b/apps/delivery/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli \ No newline at end of file diff --git a/apps/delivery/app.json b/apps/delivery/app.json new file mode 100644 index 00000000..1feead35 --- /dev/null +++ b/apps/delivery/app.json @@ -0,0 +1,40 @@ +{ + "expo": { + "name": "delivery", + "slug": "delivery", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "myapp", + "userInterfaceStyle": "automatic", + "splash": { + "image": "./assets/images/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + "expo-font" + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/apps/delivery/app/(app)/_layout.tsx b/apps/delivery/app/(app)/_layout.tsx new file mode 100644 index 00000000..4999b002 --- /dev/null +++ b/apps/delivery/app/(app)/_layout.tsx @@ -0,0 +1,158 @@ +import { useAuth } from "@/hooks/useAuth"; +import { DataProvider } from "@/hooks/useData"; +import { MaterialIcons } from "@expo/vector-icons"; +import { DrawerContentScrollView, DrawerItem } from "@react-navigation/drawer"; +import { Image } from "expo-image"; +import { useNavigation } from "expo-router"; +import { Drawer } from "expo-router/drawer"; +import { StyleSheet, Text, View } from "react-native"; + +export default function Layout() { + return ( + // + + {/* @ts-ignore */} + }> + ( + + ), + title: "overview", + headerShown: false, + }} + /> + ( + + ), + title: "order", + headerShown: false, + }} + /> + + ( + + ), + title: "Paramètres", + headerShown: false, + }} + /> + ( + + ), + title: "Profil", + headerShown: false, + }} + /> + + + // + ); +} + +function DrawerContent({ ...props }: typeof DrawerContentScrollView) { + const { user, logout } = useAuth(); + const navigation = useNavigation() as { + navigate: (href: string, params?: any) => void; + }; + + return ( + + + + + {user?.firstName} + {/*@trensik*/} + + + ( + + )} + label="Accueil" + onPress={() => navigation.navigate("index")} + /> + ( + + )} + label="Profil" + onPress={() => navigation.navigate("profile", { id: user?.id })} + /> + + + ( + + )} + label="Paramètres" + onPress={() => navigation.navigate("settings")} + /> + + + + ( + + )} + label="Se déconnecter" + onPress={() => logout()} + /> + + + + ); +} + +const styles = StyleSheet.create({ + drawerContent: { + flex: 1, + }, + userInfoSection: { + paddingLeft: 20, + flexDirection: "column", + }, + title: { + marginTop: 20, + fontWeight: "900", + fontSize: 32, + }, + caption: { + fontSize: 14, + lineHeight: 14, + }, + row: { + marginTop: 20, + flexDirection: "row", + alignItems: "center", + }, + section: { + flexDirection: "row", + alignItems: "center", + marginRight: 15, + }, + paragraph: { + fontWeight: "bold", + marginRight: 3, + }, + drawerSection: { + marginTop: 15, + }, +}); diff --git a/apps/delivery/app/(app)/index.tsx b/apps/delivery/app/(app)/index.tsx new file mode 100644 index 00000000..eb188e61 --- /dev/null +++ b/apps/delivery/app/(app)/index.tsx @@ -0,0 +1,276 @@ +import { Header } from "@/components/header"; +import { LargeLoader } from "@/components/loader/large"; +import { OrderListHeader } from "@/components/order/list-header"; +import { OrderListItem } from "@/components/order/list-item"; +import { useData } from "@/hooks/useData"; +import { useNative } from "@/hooks/useNative"; +import { MaterialIcons } from "@expo/vector-icons"; +import { Image } from "expo-image"; +import React, { useRef, useState } from "react"; +import { + Dimensions, + FlatList, + RefreshControl, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import MapView, { Marker } from "react-native-maps"; + + + +const { height } = Dimensions.get("window"); +const BOTTOM_PANEL_HEIGHT_MINIMIZED = 350; +const BOTTOM_PANEL_HEIGHT_MAXIMIZED = height - 300; + +const MARKER_DIFF_MINIMIZED = + (3.5 * BOTTOM_PANEL_HEIGHT_MINIMIZED) / height / 100; + +const MARKER_DIFF_MAXIMIZED = + (3.5 * BOTTOM_PANEL_HEIGHT_MAXIMIZED) / height / 100; + +const deltas = { latitudeDelta: 0.05, longitudeDelta: 0.05 }; + +export default function Index() { + const { location, locationPermission } = useNative(); + const { orders, refetchOrders, isOrdersLoading } = useData(); + const user_location = + location?.coords.latitude && location?.coords.longitude + ? [location?.coords.latitude, location?.coords.longitude] + : [NaN, NaN]; + + const mapRef = useRef(null); + + const [isBottomPanelMaximized, setIsBottomPanelMaximized] = useState(false); + + if (user_location.every(isNaN)) return ; + + return ( + + + {orders.map((order) => { + const { + delivery: { + address: { lat, lng }, + }, + } = order; + return ( + + + + ); + })} + +
+ + + + { + if (isBottomPanelMaximized) { + mapRef.current?.animateToRegion({ + latitude: user_location[0] - MARKER_DIFF_MINIMIZED, + longitude: user_location[1], + ...deltas, + }); + setIsBottomPanelMaximized(false); + } else { + mapRef.current?.animateToRegion({ + latitude: user_location[0] - MARKER_DIFF_MAXIMIZED, + longitude: user_location[1], + ...deltas, + }); + setIsBottomPanelMaximized(true); + } + }} + > + + + { + { + mapRef.current?.animateToRegion({ + latitude: + user_location[0] - + (isBottomPanelMaximized + ? MARKER_DIFF_MAXIMIZED + : MARKER_DIFF_MINIMIZED), + longitude: user_location[1], + ...deltas, + }); + }} + > + + + } + + + + Vous êtes connecté.e + + + + + Votre localisation est{" "} + {locationPermission?.status === "granted" + ? "activée" + : "désactivée"} + + + + + + } + data={orders} + renderItem={({ item, index }) => ( + + )} + keyExtractor={(item) => item.id} + ListHeaderComponent={OrderListHeader} + ListEmptyComponent={() => ( + + + Aucune commande pour le moment + + + )} + /> + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + map: { + width: "100%", + height: "100%", + }, + + bottom: { + position: "absolute", + bottom: 0, + left: 0, + alignItems: "flex-end", + width: "100%", + flexDirection: "column", + flex: 1, + }, + + bottom_panel: { + width: "100%", + backgroundColor: "black", + height: BOTTOM_PANEL_HEIGHT_MINIMIZED, + flexDirection: "column", + padding: 16, + color: "white", + }, + + bottom_recenter_button: { + backgroundColor: "black", + width: 50, + height: 50, + justifyContent: "center", + alignItems: "center", + }, + bottom_maximize_button: { + backgroundColor: "black", + width: 50, + height: 50, + justifyContent: "center", + alignItems: "center", + }, +}); diff --git a/apps/delivery/app/(app)/order/[id].tsx b/apps/delivery/app/(app)/order/[id].tsx new file mode 100644 index 00000000..ece6daa6 --- /dev/null +++ b/apps/delivery/app/(app)/order/[id].tsx @@ -0,0 +1,379 @@ +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import { useLocalSearchParams, useNavigation } from "expo-router"; +import { useEffect, useMemo, useState } from "react"; +import { Keyboard, Text, View } from "react-native"; +import MapView, { Marker } from "react-native-maps"; + +import { Header } from "@/components/header"; +import { Button } from "@/components/ui/button"; +import { useNative } from "@/hooks/useNative"; +import { PaymentStatus } from "@/types/payment"; + +import { useAuth } from "@/hooks/useAuth"; +import { useData } from "@/hooks/useData"; +import { calculateDistance } from "@/lib/distance"; +import { fetchAPI } from "@/lib/fetchAPI"; +import { Status } from "@/types/global"; +import { Image } from "expo-image"; +import Dialog from "react-native-dialog"; +import MapViewDirections from "react-native-maps-directions"; +import Toast from "react-native-root-toast"; + +const GOOGLE_MAPS_APIKEY = process.env.EXPO_PUBLIC_GOOGLE_MAPS_APIKEY || ""; + +export default function OrderIdScreen() { + const { session } = useAuth(); + const { token } = session || {}; + const { navigate, goBack } = useNavigation(); + const { id } = useLocalSearchParams(); + const { orders, isOrdersLoading } = useData(); + + const order = orders.find((o) => o.id === id); + + const { delivery, payment, restaurant } = order || {}; + const { eta, address } = delivery || {}; + + const address_location: [number, number] = + address?.lat !== undefined && address?.lng !== undefined + ? [address.lat, address.lng] + : [NaN, NaN]; + + const { address: restaurant_address } = restaurant || {}; + const restaurant_location: [number, number] = + restaurant_address?.lat !== undefined && + restaurant_address?.lng !== undefined + ? [restaurant_address.lat, restaurant_address.lng] + : [NaN, NaN]; + + const { location } = useNative(); + const delivery_person_location: [number, number] = + location?.coords.latitude && location?.coords.longitude + ? [location?.coords.latitude, location?.coords.longitude] + : [0, 0]; + + const region = useMemo(() => { + const avg_latitude = + (delivery_person_location[0] + + address_location[0] + + restaurant_location[0]) / + 3; + const avg_longitude = + (delivery_person_location[1] + + address_location[1] + + restaurant_location[1]) / + 3; + + const delta_lat = Math.max( + Math.abs(delivery_person_location[0] - address_location[0]), + Math.abs(delivery_person_location[0] - restaurant_location[0]), + Math.abs(address_location[0] - restaurant_location[0]) + ); + + const delta_long = Math.max( + Math.abs(delivery_person_location[1] - address_location[1]), + Math.abs(delivery_person_location[1] - restaurant_location[1]), + Math.abs(address_location[1] - restaurant_location[1]) + ); + + return { + latitude: avg_latitude - delta_lat / 3.5, + longitude: avg_longitude, + latitudeDelta: delta_lat * 2, + longitudeDelta: delta_long * 2, + }; + }, [delivery_person_location, address_location, restaurant_location]); + + const [validateDialogVisible, setValidateDialogVisible] = useState(false); + const [validateCode, setValidateCode] = useState(""); + + const [havePassedRestaurant, setHavePassedRestaurant] = useState(false); + useEffect(() => { + const distanceToRestaurant = calculateDistance( + delivery_person_location, + restaurant_location + ); + if (distanceToRestaurant < 0.1) setHavePassedRestaurant(true); + }, [delivery_person_location, restaurant_location]); + + if (isOrdersLoading) return Loading...; + if (!order) return Order not found; + + return ( + + + {!delivery_person_location.every(isNaN) && ( + + + + )} + + {!address_location.every(isNaN) && ( + + + + )} + + {!restaurant_location.every(isNaN) && ( + + + + )} + + {!havePassedRestaurant && ( + + )} + + +
+ + + {order.status !== Status.FULFILLED ? ( + eta && ( + {`Arrivée estimée à ${ + new Date(eta).toLocaleString("fr-FR", { + hour: "numeric", + minute: "numeric", + }) || "calcul en cours..." + }`} + ) + ) : ( + + Commande livrée 🧑‍🍳 + + )} + + + + + Commande à aller chercher + + {`${restaurant_address?.street} ${restaurant_address?.zipcode} ${restaurant_address?.city}`} + + + + + + + Commande à livrer + + {`${address?.street} ${address?.zipcode} ${address?.city}`} + + + + + + + {payment?.status === PaymentStatus.APPROVED + ? `La commande a été payée` + : "La commande n'a pas encore été payée"} + + + + +