diff --git a/App.js b/App.js
index 7e2af9c..9158e4b 100644
--- a/App.js
+++ b/App.js
@@ -1,16 +1,13 @@
import React from 'react';
import { StatusBar, SafeAreaView } from 'react-native';
import AppNavigator from './navigation/AppNavigator';
-import Auth from './components/Auth';
+import { FavoriteProvider } from './contexts/FavoriteContext';
export default function App() {
return (
- <>
+
-
-
-
- >
+
);
}
\ No newline at end of file
diff --git a/app.json b/app.json
index e186769..c20e83a 100644
--- a/app.json
+++ b/app.json
@@ -4,11 +4,11 @@
"slug": "LoetoLink",
"version": "1.0.0",
"orientation": "portrait",
- "icon": "./assets/icon.png",
+ "icon": "./assets/logo.jpg",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
- "image": "./assets/splash-icon.png",
+ "image": "./assets/logo.jpg",
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"updates": {
@@ -21,17 +21,17 @@
"android": {
"config": {
"googleMaps": {
- "apiKey": "AlzaSya0Ll6laKMO06oO9lBmTdqQ6P_4Qo1_fEJ"
+ "apiKey": "AlzaSy0csWCFtrxT-TmMw4adcHN41jNcy0mdvdf"
}
},
"adaptiveIcon": {
- "foregroundImage": "./assets/adaptive-icon.png",
+ "foregroundImage": "./assets/logo.jpg",
"backgroundColor": "#ffffff"
},
"package": "com.anonymous.LoetoLink"
},
"web": {
- "favicon": "./assets/favicon.png"
+ "favicon": "./assets/logo.jpg"
}
}
-}
\ No newline at end of file
+}
diff --git a/assets/avatar.jpg b/assets/avatar.jpg
new file mode 100644
index 0000000..ec5f7e8
Binary files /dev/null and b/assets/avatar.jpg differ
diff --git a/assets/background.jpg b/assets/background.jpg
new file mode 100644
index 0000000..c50bb8d
Binary files /dev/null and b/assets/background.jpg differ
diff --git a/assets/background2.jpg b/assets/background2.jpg
new file mode 100644
index 0000000..120289e
Binary files /dev/null and b/assets/background2.jpg differ
diff --git a/components/Auth.js b/components/Auth.js
index aa5a353..1de3ea5 100644
--- a/components/Auth.js
+++ b/components/Auth.js
@@ -1,43 +1,10 @@
-import React, { useState } from 'react';
-import { Alert, StyleSheet, View } from 'react-native';
-import { supabase } from '../lib/supabase'; // Adjust the import path as necessary
-import { Button, Input } from '@rneui/themed';
-
-export default function Auth() {
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
-
- const signInWithEmail = async () => {
- const { error } = await supabase.auth.signInWithPassword({ email, password });
- if (error) Alert.alert(error.message);
- };
-
- const signUpWithEmail = async () => {
- const { error } = await supabase.auth.signUp({ email, password });
- if (error) Alert.alert(error.message);
- };
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+export default function Auth({ navigation }) {
return (
-
-
-
-
+
);
}
diff --git a/components/CustomPopup.js b/components/CustomPopup.js
new file mode 100644
index 0000000..75ad94f
--- /dev/null
+++ b/components/CustomPopup.js
@@ -0,0 +1,106 @@
+import React, { useEffect, useRef } from 'react';
+import { Modal, View, Text, StyleSheet, TouchableOpacity, Animated, TouchableWithoutFeedback } from 'react-native';
+
+const CustomPopup = ({
+ visible,
+ title,
+ message,
+ onClose,
+ confirmText = "OK",
+ onConfirm,
+ children,
+}) => {
+ const scaleAnim = useRef(new Animated.Value(0.8)).current;
+ const opacityAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ if (visible) {
+ Animated.parallel([
+ Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true }),
+ Animated.timing(opacityAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
+ ]).start();
+ } else {
+ scaleAnim.setValue(0.8);
+ opacityAnim.setValue(0);
+ }
+ }, [visible]);
+
+ return (
+
+
+
+
+
+ {title ? {title} : null}
+ {message ? {message} : null}
+ {children}
+ {!children && (
+
+ {confirmText}
+
+ )}
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.18)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ popup: {
+ width: 300,
+ backgroundColor: '#fff',
+ borderRadius: 14,
+ paddingVertical: 22,
+ paddingHorizontal: 14,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOpacity: 0.10,
+ shadowRadius: 10,
+ shadowOffset: { width: 0, height: 4 },
+ elevation: 6,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginBottom: 6,
+ color: '#222',
+ textAlign: 'center',
+ backgroundColor: 'transparent',
+ padding: 0,
+ },
+ message: {
+ fontSize: 15,
+ color: '#444',
+ marginBottom: 14,
+ textAlign: 'center',
+ backgroundColor: 'transparent',
+ padding: 0,
+ },
+ button: {
+ backgroundColor: '#418EDA',
+ borderRadius: 100,
+ paddingVertical: 9,
+ paddingHorizontal: 32,
+ alignItems: 'center',
+ marginTop: 8,
+ shadowColor: '#418EDA',
+ shadowOpacity: 0.13,
+ shadowRadius: 6,
+ elevation: 3,
+ },
+ buttonText: {
+ color: '#fff',
+ fontWeight: 'bold',
+ fontSize: 15,
+ },
+});
+
+export default CustomPopup;
\ No newline at end of file
diff --git a/components/StopListComponent.js b/components/StopListComponent.js
new file mode 100644
index 0000000..d2639c1
--- /dev/null
+++ b/components/StopListComponent.js
@@ -0,0 +1,145 @@
+import React from 'react';
+import { View, Text, StyleSheet, FlatList } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+
+const formatDistance = (meters) => {
+ if (meters == null) return '';
+ if (meters < 1000) return `${meters} m`;
+ return `${(meters / 1000).toFixed(1)} km`;
+};
+
+const StopListComponent = ({ routeWaypoints }) => {
+ if (!routeWaypoints || routeWaypoints.length === 0) {
+ return No stops available.;
+ }
+
+ return (
+ idx.toString()}
+ renderItem={({ item, index }) => (
+
+ {/* Timeline */}
+
+ {/* Top line */}
+ {index !== 0 && }
+ {/* Circle */}
+
+ {/* Bottom line */}
+ {index !== routeWaypoints.length - 1 && }
+
+ {/* Stop info */}
+
+
+ {item.time || '--:--'}
+ {item.eta && (
+ ({item.eta})
+ )}
+ {item.delay && (
+ +{item.delay} min
+ )}
+
+ {item.name}
+ {item.platform && (
+ Platform: {item.platform}
+ )}
+ {item.distance != null && (
+
+ {formatDistance(item.distance)} from you
+
+ )}
+
+
+ )}
+ />
+ );
+};
+
+const styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginBottom: 32, // Increased spacing between stops
+ minHeight: 56, // Increased minimum height for each row
+ },
+ timeline: {
+ width: 40, // Increased width for timeline area
+ alignItems: 'center',
+ position: 'relative',
+ },
+ line: {
+ width: 3, // Thicker line
+ height: 28, // Longer line for more spacing
+ backgroundColor: '#018abe',
+ position: 'absolute',
+ left: 18.5, // Centered for bigger circle
+ zIndex: 0,
+ },
+ circle: {
+ width: 28, // Larger icon
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: '#fff',
+ borderWidth: 5,
+ borderColor: '#018abe',
+ marginVertical: 0,
+ zIndex: 1,
+ marginTop: 0,
+ marginBottom: 0,
+ },
+ circleActive: {
+ backgroundColor: '#018abe',
+ },
+ circleTerminal: {
+ borderColor: '#43a047',
+ },
+ info: {
+ marginLeft: 18, // More space between icon and text
+ flex: 1,
+ borderBottomWidth: 1,
+ borderBottomColor: '#f0f0f0',
+ paddingBottom: 10,
+ },
+ time: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#018abe',
+ },
+ eta: {
+ fontSize: 15,
+ color: '#888',
+ marginLeft: 4,
+ },
+ delay: {
+ fontSize: 15,
+ color: '#d32f2f',
+ marginLeft: 4,
+ fontWeight: 'bold',
+ },
+ name: {
+ fontSize: 18,
+ fontWeight: '500',
+ color: '#222',
+ marginTop: 4,
+ },
+ platform: {
+ fontSize: 14,
+ color: '#888',
+ marginTop: 2,
+ },
+ distance: {
+ fontSize: 13,
+ color: '#018abe',
+ marginTop: 2,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+});
+
+export default StopListComponent;
\ No newline at end of file
diff --git a/components/UserHeader.js b/components/UserHeader.js
new file mode 100644
index 0000000..2b570e2
--- /dev/null
+++ b/components/UserHeader.js
@@ -0,0 +1,89 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
+import { supabase } from '../lib/supabase';
+import { Ionicons } from '@expo/vector-icons';
+
+const fallbackAvatar = require('../assets/avatar.jpg');
+
+const UserHeader = ({ onAvatarPress }) => {
+ const [user, setUser] = useState(null);
+
+ useEffect(() => {
+ const fetchUser = async () => {
+ const { data } = await supabase.auth.getUser();
+ if (data?.user) {
+ const { user } = data;
+ const displayName =
+ user.user_metadata?.full_name ||
+ user.user_metadata?.name ||
+ user.email;
+ const avatar =
+ user.user_metadata?.avatar_url ||
+ user.user_metadata?.picture ||
+ null;
+ setUser({
+ name: displayName,
+ avatar,
+ });
+ }
+ };
+ fetchUser();
+ }, []);
+
+ // Fix the avatar URL logic
+ const getAvatarSource = () => {
+ if (user?.avatar) {
+ return { uri: user.avatar };
+ }
+ return fallbackAvatar;
+ };
+
+ return (
+
+
+
+
+
+ Hey, {user?.name || 'User'}
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ margin: 10,
+ marginTop: 40,
+ padding: 16,
+ borderRadius: 16,
+ shadowColor: '#000',
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 2,
+ justifyContent: 'space-between',
+ },
+ avatar: {
+ width: 40,
+ height: 40,
+ borderRadius: 12,
+ marginRight: 16,
+ },
+ greeting: {
+ flex: 1,
+ fontSize: 18,
+ fontWeight: '500',
+ color: '#333',
+ textAlign: 'center',
+ },
+ name: {
+ fontWeight: 'bold',
+ },
+});
+
+export default UserHeader;
\ No newline at end of file
diff --git a/contexts/FavoriteContext.js b/contexts/FavoriteContext.js
new file mode 100644
index 0000000..58d6d7e
--- /dev/null
+++ b/contexts/FavoriteContext.js
@@ -0,0 +1,20 @@
+import React, { createContext, useContext, useState, useCallback } from 'react';
+
+const FavoriteContext = createContext();
+
+export const useFavorite = () => useContext(FavoriteContext);
+
+export const FavoriteProvider = ({ children }) => {
+ const [favoriteChanged, setFavoriteChanged] = useState(0);
+
+ // Call this after add/remove favorite
+ const notifyFavoriteChanged = useCallback(() => {
+ setFavoriteChanged(c => c + 1);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/contexts/ThemeContext.js b/contexts/ThemeContext.js
new file mode 100644
index 0000000..1c41e73
--- /dev/null
+++ b/contexts/ThemeContext.js
@@ -0,0 +1,37 @@
+import React, { createContext, useState, useContext } from 'react';
+
+const ThemeContext = createContext();
+
+const lightTheme = {
+ background: '#f6f7fa',
+ card: '#fff',
+ text: '#222',
+ button: '#2d9cdb',
+ buttonText: '#fff',
+ input: '#fff',
+ inputText: '#222',
+};
+
+const darkTheme = {
+ background: '#000',
+ card: '#181818',
+ text: '#fff',
+ button: '#fff',
+ buttonText: '#000',
+ input: '#222',
+ inputText: '#fff',
+};
+
+export const ThemeProvider = ({ children }) => {
+ const [dark, setDark] = useState(false);
+ const theme = dark ? darkTheme : lightTheme;
+ const toggleTheme = () => setDark((d) => !d);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTheme = () => useContext(ThemeContext);
\ No newline at end of file
diff --git a/fonts/jgs.ttf b/fonts/jgs.ttf
new file mode 100644
index 0000000..a2a734c
Binary files /dev/null and b/fonts/jgs.ttf differ
diff --git a/navigation/AppNavigator.js b/navigation/AppNavigator.js
index 84c194a..9b44874 100644
--- a/navigation/AppNavigator.js
+++ b/navigation/AppNavigator.js
@@ -1,10 +1,11 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
-import { NavigationContainer } from '@react-navigation/native';
+import { NavigationContainer, useNavigation, useRoute } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Ionicons } from '@expo/vector-icons';
-import { Animated } from 'react-native';
+import { Animated, TouchableOpacity, View, StyleSheet } from 'react-native';
import { CardStyleInterpolators } from '@react-navigation/stack';
+import { useFocusEffect } from '@react-navigation/native';
// Auth Screens
import SplashScreen from '../screens/SplashScreen';
@@ -14,86 +15,230 @@ import LoginScreen from '../screens/LoginScreen';
import HomeScreen from '../screens/HomeScreen';
import MapViewScreen from '../screens/MapViewScreen';
import BusStopScreen from '../screens/BusStopScreen';
-import TransportInfoScreen from '../screens/TransportInfoScreen';
import FavoritesScreen from '../screens/FavoritesScreen';
import ProfileScreen from '../screens/ProfileScreen';
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
-// Main tab navigator for the app
-const MainTabNavigator = () => {
- const tabBarAnimation = new Animated.Value(1);
+const NAV_ICONS = [
+ { name: 'home', screen: 'Home', component: HomeScreen },
+ { name: 'map', screen: 'MapViewScreen', component: MapViewScreen },
+ { name: 'bus', screen: 'Bus Stops', component: BusStopScreen },
+ { name: 'heart', screen: 'Favorites', component: FavoritesScreen },
+ { name: 'person', screen: 'Profile', component: ProfileScreen },
+];
+
+// Floating animated navigation bar
+const FloatingNavBar = () => {
+ const navigation = useNavigation();
+ const route = useRoute();
+ const [expanded, setExpanded] = useState(false);
+ const anim = useRef(new Animated.Value(0)).current;
+ const iconOpacity = useRef(new Animated.Value(1)).current;
+ const timerRef = useRef(null);
+ const fadeTimerRef = useRef(null);
+
+ const animateNavBar = (toValue) => {
+ Animated.timing(anim, {
+ toValue,
+ duration: 350,
+ useNativeDriver: false,
+ }).start();
+ };
+
+ const fadeIcons = (toValue) => {
+ Animated.timing(iconOpacity, {
+ toValue,
+ duration: 2000,
+ useNativeDriver: true,
+ }).start();
+ };
+
+ const resetIcons = () => {
+ iconOpacity.setValue(1);
+ if (fadeTimerRef.current) {
+ clearTimeout(fadeTimerRef.current);
+ fadeTimerRef.current = null;
+ }
+ };
+
+const collapse = () => {
+ setExpanded(false);
+ animateNavBar(0);
+ // Do NOT call resetIcons() here!
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+};
+
+const toggle = () => {
+ if (!expanded) {
+ setExpanded(true);
+ animateNavBar(1);
+ resetIcons(); // Only reset opacity when expanding
+ if (timerRef.current) clearTimeout(timerRef.current);
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
+
+ // Start fade-out 8 seconds after expanding (so icons fade for 2 seconds before collapse)
+ fadeTimerRef.current = setTimeout(() => {
+ fadeIcons(0);
+ }, 8000);
+
+ timerRef.current = setTimeout(() => {
+ collapse();
+ }, 10000); // 10 seconds
+ } else {
+ collapse();
+ }
+};
+
+// ...existing code...
+
+ const navigateToScreen = (screen) => {
+ collapse();
+ if (route.name === screen) return; // Already on the target screen
+ if (NAV_ICONS.some(icon => icon.screen === route.name)) {
+ navigation.navigate(screen); // Already in tab navigator
+ } else {
+ navigation.navigate('MainTabNavigator', { screen });
+ }
+ };
+
+ // Clean up timers on unmount
+ React.useEffect(() => {
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
+ };
+ }, []);
+
+ // Animate width and borderRadius
+ const width = anim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [64, 340],
+ });
+ const borderRadius = anim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [32, 28],
+ });
return (
- ({
- headerShown: false,
- tabBarIcon: ({ focused, color, size }) => {
- let iconName;
-
- if (route.name === 'Home') {
- iconName = focused ? 'home' : 'home-outline';
- } else if (route.name === 'Map') {
- iconName = focused ? 'map' : 'map-outline';
- } else if (route.name === 'Bus Stops') {
- iconName = focused ? 'bus' : 'bus-outline';
- } else if (route.name === 'Transport') {
- iconName = focused ? 'information-circle' : 'information-circle-outline';
- } else if (route.name === 'Favorites') {
- iconName = focused ? 'heart' : 'heart-outline';
- } else if (route.name === 'Profile') {
- iconName = focused ? 'person' : 'person-outline';
- }
-
- return ;
+
-
-
-
-
-
-
-
+ {expanded ? (
+
+ {NAV_ICONS.map((icon) => (
+ navigateToScreen(icon.screen)}
+ >
+
+
+ ))}
+
+ ) : (
+
+
+
+ )}
+ {/* Removed the close (X) button */}
+
+ );
+};
+
+const navStyles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ bottom: 30,
+ alignSelf: 'center',
+ height: 64,
+ flexDirection: 'row',
+ alignItems: 'center',
+ zIndex: 100,
+ elevation: 10,
+ shadowColor: '#000',
+ shadowOpacity: 0.2,
+ shadowRadius: 8,
+ paddingHorizontal: 12,
+ },
+ iconRow: {
+ flexDirection: 'row',
+ flex: 1,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ iconButton: {
+ marginHorizontal: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ homeButton: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ closeButton: {
+ position: 'absolute',
+ right: 10,
+ top: 10,
+ backgroundColor: 'rgba(0,0,0,0.2)',
+ borderRadius: 12,
+ padding: 2,
+ },
+});
+
+// Main tab navigator for the app
+const MainTabNavigator = () => {
+ return (
+ <>
+
+ {NAV_ICONS.map((icon) => (
+
+ ))}
+
+
+ >
);
};
// Auth stack navigator
const AuthStack = () => {
return (
-
@@ -109,10 +254,8 @@ const RootNavigator = () => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
- // Simulate checking authentication status
setTimeout(() => {
setIsLoading(false);
- // For development, you can set this to true to skip login
setIsAuthenticated(false);
}, 1000);
}, []);
@@ -122,12 +265,12 @@ const RootNavigator = () => {
}
return (
-
{isAuthenticated ? (
diff --git a/package-lock.json b/package-lock.json
index c52d009..165dd1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,11 +17,20 @@
"@rneui/themed": "^4.0.0-rc.8",
"@supabase/supabase-js": "^2.49.4",
"expo": "~52.0.46",
+ "expo-apple-authentication": "~7.1.3",
+ "expo-auth-session": "~6.0.3",
+ "expo-location": "~18.0.10",
"expo-status-bar": "~2.0.1",
+ "expo-web-browser": "~14.0.2",
+ "geolib": "^3.3.4",
+ "haversine-distance": "^1.2.3",
+ "lodash": "^4.17.21",
"lottie-react-native": "^7.1.0",
"react": "18.3.1",
"react-native": "0.76.9",
"react-native-maps": "1.18.0",
+ "react-native-modal": "^14.0.0-rc.1",
+ "react-native-popup-dialog": "^0.18.3",
"react-native-safe-area-context": "^4.12.0",
"react-native-screens": "~4.4.0",
"react-native-url-polyfill": "^2.0.0"
@@ -5710,6 +5719,87 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/babel-code-frame": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+ "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^1.1.3",
+ "esutils": "^2.0.2",
+ "js-tokens": "^3.0.2"
+ }
+ },
+ "node_modules/babel-code-frame/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/babel-code-frame/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/babel-code-frame/node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/babel-code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/babel-code-frame/node_modules/js-tokens": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+ "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==",
+ "license": "MIT"
+ },
+ "node_modules/babel-code-frame/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/babel-code-frame/node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/babel-core": {
"version": "7.0.0-bridge.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
@@ -5719,6 +5809,41 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/babel-generator": {
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+ "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "detect-indent": "^4.0.0",
+ "jsesc": "^1.3.0",
+ "lodash": "^4.17.4",
+ "source-map": "^0.5.7",
+ "trim-right": "^1.0.1"
+ }
+ },
+ "node_modules/babel-generator/node_modules/jsesc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+ "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ }
+ },
+ "node_modules/babel-helpers": {
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+ "integrity": "sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-runtime": "^6.22.0",
+ "babel-template": "^6.24.1"
+ }
+ },
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -5740,6 +5865,93 @@
"@babel/core": "^7.8.0"
}
},
+ "node_modules/babel-messages": {
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+ "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-runtime": "^6.22.0"
+ }
+ },
+ "node_modules/babel-plugin-flow-react-proptypes": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-flow-react-proptypes/-/babel-plugin-flow-react-proptypes-9.2.0.tgz",
+ "integrity": "sha512-gmClrDpTB1H27mh+6/8iliV5BzGic5F9DO7INAd/30sSkg4XZd7LTbv4z06usuUHS8SzXibwotk8ct51IZ5OaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-core": "^6.25.0",
+ "babel-template": "^6.25.0",
+ "babel-traverse": "^6.25.0",
+ "babel-types": "^6.25.0"
+ }
+ },
+ "node_modules/babel-plugin-flow-react-proptypes/node_modules/babel-core": {
+ "version": "6.26.3",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+ "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-code-frame": "^6.26.0",
+ "babel-generator": "^6.26.0",
+ "babel-helpers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-register": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "convert-source-map": "^1.5.1",
+ "debug": "^2.6.9",
+ "json5": "^0.5.1",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.4",
+ "path-is-absolute": "^1.0.1",
+ "private": "^0.1.8",
+ "slash": "^1.0.0",
+ "source-map": "^0.5.7"
+ }
+ },
+ "node_modules/babel-plugin-flow-react-proptypes/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/babel-plugin-flow-react-proptypes/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/babel-plugin-flow-react-proptypes/node_modules/json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/babel-plugin-flow-react-proptypes/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/babel-plugin-flow-react-proptypes/node_modules/slash": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+ "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/babel-plugin-istanbul": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
@@ -5920,6 +6132,187 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/babel-register": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+ "integrity": "sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-core": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "core-js": "^2.5.0",
+ "home-or-tmp": "^2.0.0",
+ "lodash": "^4.17.4",
+ "mkdirp": "^0.5.1",
+ "source-map-support": "^0.4.15"
+ }
+ },
+ "node_modules/babel-register/node_modules/babel-core": {
+ "version": "6.26.3",
+ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+ "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-code-frame": "^6.26.0",
+ "babel-generator": "^6.26.0",
+ "babel-helpers": "^6.24.1",
+ "babel-messages": "^6.23.0",
+ "babel-register": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "babel-template": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "convert-source-map": "^1.5.1",
+ "debug": "^2.6.9",
+ "json5": "^0.5.1",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.4",
+ "path-is-absolute": "^1.0.1",
+ "private": "^0.1.8",
+ "slash": "^1.0.0",
+ "source-map": "^0.5.7"
+ }
+ },
+ "node_modules/babel-register/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/babel-register/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/babel-register/node_modules/json5": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+ "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/babel-register/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/babel-register/node_modules/slash": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+ "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/babel-register/node_modules/source-map-support": {
+ "version": "0.4.18",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+ "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "^0.5.6"
+ }
+ },
+ "node_modules/babel-runtime": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+ "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
+ "license": "MIT",
+ "dependencies": {
+ "core-js": "^2.4.0",
+ "regenerator-runtime": "^0.11.0"
+ }
+ },
+ "node_modules/babel-runtime/node_modules/regenerator-runtime": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+ "license": "MIT"
+ },
+ "node_modules/babel-template": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+ "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-runtime": "^6.26.0",
+ "babel-traverse": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "lodash": "^4.17.4"
+ }
+ },
+ "node_modules/babel-traverse": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+ "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-code-frame": "^6.26.0",
+ "babel-messages": "^6.23.0",
+ "babel-runtime": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "babylon": "^6.18.0",
+ "debug": "^2.6.8",
+ "globals": "^9.18.0",
+ "invariant": "^2.2.2",
+ "lodash": "^4.17.4"
+ }
+ },
+ "node_modules/babel-traverse/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/babel-traverse/node_modules/globals": {
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/babel-traverse/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/babel-types": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+ "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-runtime": "^6.26.0",
+ "esutils": "^2.0.2",
+ "lodash": "^4.17.4",
+ "to-fast-properties": "^1.0.3"
+ }
+ },
+ "node_modules/babylon": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+ "license": "MIT",
+ "bin": {
+ "babylon": "bin/babylon.js"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -6709,6 +7102,14 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"license": "MIT"
},
+ "node_modules/core-js": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
+ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
+ "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+ "hasInstallScript": true,
+ "license": "MIT"
+ },
"node_modules/core-js-compat": {
"version": "3.41.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
@@ -6929,6 +7330,18 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==",
+ "license": "MIT",
+ "dependencies": {
+ "repeating": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -7190,7 +7603,6 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7359,6 +7771,25 @@
}
}
},
+ "node_modules/expo-apple-authentication": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/expo-apple-authentication/-/expo-apple-authentication-7.1.3.tgz",
+ "integrity": "sha512-TRaF513oDGjGx3hRiAwkMiSnKLN8BIR9Se5Gi3ttz2UUgP9y+tNHV6Ji6/oztJo9ON7zerHg2mn5Y+3B8c2vTQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-application": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.0.2.tgz",
+ "integrity": "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-asset": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.5.tgz",
@@ -7376,6 +7807,24 @@
"react-native": "*"
}
},
+ "node_modules/expo-auth-session": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-6.0.3.tgz",
+ "integrity": "sha512-s7LmmMPiiY1NXrlcXkc4+09Hlfw9X1CpaQOCDkwfQEodG1uCYGQi/WImTnDzw5YDkWI79uC8F1mB8EIerilkDA==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-application": "~6.0.2",
+ "expo-constants": "~17.0.5",
+ "expo-crypto": "~14.0.2",
+ "expo-linking": "~7.0.5",
+ "expo-web-browser": "~14.0.2",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-constants": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz",
@@ -7390,6 +7839,18 @@
"react-native": "*"
}
},
+ "node_modules/expo-crypto": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-14.0.2.tgz",
+ "integrity": "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-file-system": {
"version": "18.0.12",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz",
@@ -7426,6 +7887,29 @@
"react": "*"
}
},
+ "node_modules/expo-linking": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.0.5.tgz",
+ "integrity": "sha512-3KptlJtcYDPWohk0MfJU75MJFh2ybavbtcSd84zEPfw9s1q3hjimw3sXnH03ZxP54kiEWldvKmmnGcVffBDB1g==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-constants": "~17.0.5",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-location": {
+ "version": "18.0.10",
+ "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-18.0.10.tgz",
+ "integrity": "sha512-R0Iioz0UZ9Ts8TACPngh8uDFbajJhVa5/igLqWB8Pq/gp8UHuwj7PC8XbZV7avsFoShYjaxrOhf4U7IONeKLgg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-modules-autolinking": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.8.tgz",
@@ -7500,6 +7984,16 @@
"react-native": "*"
}
},
+ "node_modules/expo-web-browser": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.0.2.tgz",
+ "integrity": "sha512-Hncv2yojhTpHbP6SGWARBFdl7P6wBHc1O8IKaNsH0a/IEakq887o1eRhLxZ5IwztPQyRDhpqHdgJ+BjWolOnwA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/exponential-backoff": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
@@ -7825,6 +8319,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/geolib": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.4.tgz",
+ "integrity": "sha512-EicrlLLL3S42gE9/wde+11uiaYAaeSVDwCUIv2uMIoRBfNJCn8EsSI+6nS3r4TCKDO6+RQNM9ayLq2at+oZQWQ==",
+ "license": "MIT"
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -8004,6 +8504,27 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-ansi/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -8052,6 +8573,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/haversine-distance": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/haversine-distance/-/haversine-distance-1.2.3.tgz",
+ "integrity": "sha512-cRKtuukt+/fVprlLueX30mytMNtiQ4tff93FDW9yrBx3BYrq6gujiw1+hEaaxssKlOZTm8R3BPtMbNM+n0+bqQ==",
+ "license": "MIT"
+ },
"node_modules/hermes-estree": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz",
@@ -8084,6 +8611,19 @@
"license": "MIT",
"peer": true
},
+ "node_modules/home-or-tmp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+ "integrity": "sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==",
+ "license": "MIT",
+ "dependencies": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/hosted-git-info": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
@@ -8356,6 +8896,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-finite": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
+ "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -9158,8 +9710,7 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
@@ -10568,6 +11119,24 @@
"node": ">=4"
}
},
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -10974,6 +11543,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/private": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+ "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/proc-log": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz",
@@ -11019,7 +11597,6 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -11030,8 +11607,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/pump": {
"version": "3.0.2",
@@ -11285,6 +11861,15 @@
}
}
},
+ "node_modules/react-native-animatable": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz",
+ "integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ }
+ },
"node_modules/react-native-gesture-handler": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.25.0.tgz",
@@ -11323,6 +11908,33 @@
}
}
},
+ "node_modules/react-native-modal": {
+ "version": "14.0.0-rc.1",
+ "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz",
+ "integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-animatable": "1.4.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": ">=0.70.0"
+ }
+ },
+ "node_modules/react-native-popup-dialog": {
+ "version": "0.18.3",
+ "resolved": "https://registry.npmjs.org/react-native-popup-dialog/-/react-native-popup-dialog-0.18.3.tgz",
+ "integrity": "sha512-ZvqixSEfMlcX2sm9rSRk/KkgWXwvnj7xqq4fIpSGlYGX48FfL8b3Xf2/uT9SvK2H7MX79b7sic53zzwJ+aBh3g==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-flow-react-proptypes": "^9.1.1",
+ "prop-types": "^15.6.0",
+ "react-native-root-siblings": "^3.2.1"
+ },
+ "peerDependencies": {
+ "react-native": ">=0.50.0"
+ }
+ },
"node_modules/react-native-ratings": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-8.1.0.tgz",
@@ -11337,6 +11949,16 @@
"react-native": "*"
}
},
+ "node_modules/react-native-root-siblings": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/react-native-root-siblings/-/react-native-root-siblings-3.2.3.tgz",
+ "integrity": "sha512-wOCCtKJteaSIW3K++hzhkfdWRikTqjrG34DnhNDVSzKatuNQyFY1fPBD1YFT/3+kxOIUmNsJdiaPMao9QgoZMA==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.6.2",
+ "static-container": "^1.0.0"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
@@ -11691,6 +12313,18 @@
"integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==",
"license": "MIT"
},
+ "node_modules/repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==",
+ "license": "MIT",
+ "dependencies": {
+ "is-finite": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -12499,6 +13133,12 @@
"node": ">=6"
}
},
+ "node_modules/static-container": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/static-container/-/static-container-1.7.1.tgz",
+ "integrity": "sha512-rsUMpoUZ4sHsFyy+Wp9Kqnv3HU+LD5ShPRd+ocg8US9juKckq+7VgDEK+ZbG8sQ1EOaw2LBpF8XLwpozqGHGBQ==",
+ "license": "MIT"
+ },
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -13027,6 +13667,15 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"license": "BSD-3-Clause"
},
+ "node_modules/to-fast-properties": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+ "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -13054,6 +13703,15 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/trim-right": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+ "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
diff --git a/package.json b/package.json
index 56a002c..92dcb61 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,20 @@
"@rneui/themed": "^4.0.0-rc.8",
"@supabase/supabase-js": "^2.49.4",
"expo": "~52.0.46",
+ "expo-apple-authentication": "~7.1.3",
+ "expo-auth-session": "~6.0.3",
+ "expo-location": "~18.0.10",
"expo-status-bar": "~2.0.1",
+ "expo-web-browser": "~14.0.2",
+ "geolib": "^3.3.4",
+ "haversine-distance": "^1.2.3",
+ "lodash": "^4.17.21",
"lottie-react-native": "^7.1.0",
"react": "18.3.1",
"react-native": "0.76.9",
"react-native-maps": "1.18.0",
+ "react-native-modal": "^14.0.0-rc.1",
+ "react-native-popup-dialog": "^0.18.3",
"react-native-safe-area-context": "^4.12.0",
"react-native-screens": "~4.4.0",
"react-native-url-polyfill": "^2.0.0"
diff --git a/screens/BusStopScreen.js b/screens/BusStopScreen.js
index ba9de66..92ec37c 100644
--- a/screens/BusStopScreen.js
+++ b/screens/BusStopScreen.js
@@ -1,30 +1,261 @@
-import React from 'react';
-import { View, Text, StyleSheet } from 'react-native';
-import Button from '../components/Button';
+import React, { useState, useEffect, useRef } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, FlatList, ActivityIndicator, Alert, Animated, ImageBackground } from 'react-native';
+import { supabase } from '../lib/supabase';
+import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
+
+// Place your image in the assets folder and update the path below
+const backgroundImage = require('../assets/background.jpg'); // Rename your image to busstop-bg.jpg and put it in assets
+
+const BusStopScreen = () => {
+ const [routes, setRoutes] = useState([]);
+ const [allStops, setAllStops] = useState([]);
+ const [expandedRouteId, setExpandedRouteId] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const animations = useRef({}).current;
+
+ useEffect(() => {
+ const fetchAll = async () => {
+ setLoading(true);
+ try {
+ const [{ data: routesData, error: routesError }, { data: stopsData, error: stopsError }] = await Promise.all([
+ supabase.from('combi_routes').select('*').order('route_name', { ascending: true }),
+ supabase.from('stops').select('*').order('stop_order', { ascending: true }),
+ ]);
+ if (routesError || stopsError) {
+ Alert.alert('Error', 'Failed to fetch routes or stops.');
+ return;
+ }
+ setRoutes(routesData);
+ setAllStops(stopsData);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchAll();
+ }, []);
+
+ const handleToggleExpand = (routeId) => {
+ if (expandedRouteId === routeId) {
+ // Collapse
+ if (animations[routeId]) {
+ Animated.timing(animations[routeId], {
+ toValue: 0,
+ duration: 250,
+ useNativeDriver: false,
+ }).start(() => setExpandedRouteId(null));
+ } else {
+ setExpandedRouteId(null);
+ }
+ } else {
+ // Expand
+ if (!animations[routeId]) {
+ animations[routeId] = new Animated.Value(0);
+ }
+ setExpandedRouteId(routeId);
+ Animated.timing(animations[routeId], {
+ toValue: 1,
+ duration: 250,
+ useNativeDriver: false,
+ }).start();
+ }
+ };
+
+ const renderStops = (routeId) => {
+ const stops = allStops.filter(stop => stop.route_id === routeId);
+ if (!animations[routeId]) {
+ animations[routeId] = new Animated.Value(0);
+ }
+ const maxHeight = stops.length * 40 + 20; // estimate height
+
+ return (
+
+ {stops.map((stop) => (
+
+ {stop.stop_order}.
+ {stop.name}
+
+ ))}
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
-const BusStopScreen = ({ navigation }) => {
return (
-
- Bus Stop Screen
-
+
+
+
+
+ Bus Stops
+
+
+ item.id.toString()}
+ renderItem={({ item }) => (
+
+
+
+
+
+ {item.route_name}
+
+ P7.00
+
+
+
+ From:
+ {item.origin}
+
+
+
+ To:
+ {item.destination}
+
+ handleToggleExpand(item.id)}
+ >
+
+ {expandedRouteId === item.id ? 'Hide Stops' : 'Show Stops'}
+
+
+
+ {(expandedRouteId === item.id) && renderStops(item.id)}
+
+ )}
+ contentContainerStyle={{ paddingBottom: 30, paddingTop: 30 }}
+ />
+
);
};
const styles = StyleSheet.create({
+ background: {
+ flex: 1,
+ width: '100%',
+ height: '100%',
+ },
container: {
flex: 1,
+ backgroundColor: 'transparent',
+ paddingTop: 0,
+ },
+ routeCard: {
+ backgroundColor: 'white',
+ marginHorizontal: 16,
+ marginBottom: 18,
+ borderRadius: 16,
+ padding: 18,
+ shadowColor: '#000',
+ shadowOpacity: 0.08,
+ shadowRadius: 8,
+ elevation: 2,
+ zIndex: 2,
+ },
+ routeHeaderRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 10,
+ },
+ busIconContainer: {
+ width: 44,
+ height: 44,
+ borderRadius: 12,
+ backgroundColor: '#e6f2fa',
+ alignItems: 'center',
justifyContent: 'center',
+ marginRight: 10,
+ },
+ routeTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: '#018abe',
+ flexShrink: 1,
+ },
+ priceText: {
+ fontSize: 16,
+ color: '#222',
+ marginLeft: 10,
+ },
+ routeDetailsRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 2,
+ marginLeft: 4,
+ },
+ detailLabel: {
+ fontWeight: 'bold',
+ color: '#555',
+ marginRight: 4,
+ fontSize: 15,
+ },
+ detailValue: {
+ color: '#333',
+ fontSize: 15,
+ flexShrink: 1,
+ },
+ dropdownToggle: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 10,
+ marginBottom: 2,
+ alignSelf: 'flex-start',
+ paddingVertical: 4,
+ paddingHorizontal: 8,
+ borderRadius: 8,
+ backgroundColor: '#018abe11',
+ },
+ dropdownToggleText: {
+ color: '#018abe',
+ fontWeight: 'bold',
+ marginRight: 4,
+ fontSize: 15,
+ },
+ stopsContainer: {
+ paddingHorizontal: 8,
+ paddingBottom: 6,
+ backgroundColor: '#f5f6fa',
+ marginTop: 6,
+ borderRadius: 8,
+ },
+ stopRow: {
+ flexDirection: 'row',
alignItems: 'center',
- backgroundColor: '#dce2ef',
- padding: 20,
+ paddingVertical: 5,
+ borderBottomWidth: 0.5,
+ borderBottomColor: '#e0e0e0',
},
- text: {
- fontSize: 24,
+ stopOrder: {
+ width: 28,
+ color: '#888',
fontWeight: 'bold',
},
- button: {
- borderRadius: 50,
+ stopName: {
+ fontSize: 16,
+ color: '#333',
},
});
diff --git a/screens/FavoritesScreen.js b/screens/FavoritesScreen.js
index 7154cf9..0d54f69 100644
--- a/screens/FavoritesScreen.js
+++ b/screens/FavoritesScreen.js
@@ -1,65 +1,435 @@
-import React from 'react';
-import { View, Text, StyleSheet, FlatList } from 'react-native';
-
-const FavoritesScreen = ({ navigation }) => {
- const favorites = [
- { id: '1', name: 'Route 1' },
- { id: '2', name: 'Route 2' },
- { id: '3', name: 'Route 3' },
- { id: '4', name: 'Route 4' },
- ];
-
- const renderItem = ({ item }) => (
-
- {item.name}
-
+import React, { useRef, useState, useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Animated, ScrollView, Dimensions, ActivityIndicator, Alert, ImageBackground } from 'react-native';
+import { Ionicons, MaterialIcons } from '@expo/vector-icons';
+import MapView from 'react-native-maps';
+import { useNavigation, useFocusEffect } from '@react-navigation/native';
+import { supabase } from '../lib/supabase';
+import { useFavorite } from '../contexts/FavoriteContext'; // <-- import context
+
+const { width } = Dimensions.get('window');
+const COLLAPSED_HEIGHT = 120;
+const EXPANDED_HEIGHT = 420;
+
+// --- Custom Popup Component ---
+const AnimatedPopup = ({ visible, message, onHide }) => {
+ const opacity = useRef(new Animated.Value(0)).current;
+ const translateY = useRef(new Animated.Value(40)).current;
+
+ useEffect(() => {
+ if (visible) {
+ Animated.parallel([
+ Animated.timing(opacity, {
+ toValue: 1,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ Animated.spring(translateY, {
+ toValue: 0,
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ // Auto-hide after 1.8s
+ const timer = setTimeout(() => {
+ Animated.parallel([
+ Animated.timing(opacity, {
+ toValue: 0,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ Animated.timing(translateY, {
+ toValue: 40,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ ]).start(() => {
+ onHide && onHide();
+ });
+ }, 1800);
+
+ return () => clearTimeout(timer);
+ }
+ }, [visible]);
+
+ if (!visible) return null;
+
+ return (
+
+ {message}
+
+ );
+};
+
+const FavoritesScreen = () => {
+ const navigation = useNavigation();
+ const [favoriteRoutes, setFavoriteRoutes] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [expandedId, setExpandedId] = useState(null);
+ const animations = useRef([]).current;
+
+ // Popup state
+ const [popup, setPopup] = useState({ visible: false, message: '' });
+
+ const { notifyFavoriteChanged } = useFavorite(); // <-- use context
+
+ useFocusEffect(
+ React.useCallback(() => {
+ const fetchFavorites = async () => {
+ setLoading(true);
+ try {
+ const { data: { user }, error: userError } = await supabase.auth.getUser();
+ if (userError || !user) {
+ setFavoriteRoutes([]);
+ setLoading(false);
+ return;
+ }
+
+ const { data, error } = await supabase
+ .from('user_favorite_routes')
+ .select('route_id, combi_routes:route_id (id, route_name, origin, destination, origin_latitude, origin_longitude)')
+ .eq('user_id', user.id);
+
+ if (error) {
+ console.error('Fetch favorites error:', error);
+ Alert.alert('Error', 'Failed to fetch favorites');
+ setFavoriteRoutes([]);
+ } else {
+ setFavoriteRoutes(
+ data.map((fav, idx) => ({
+ id: fav.combi_routes.id,
+ name: fav.combi_routes.route_name,
+ origin: fav.combi_routes.origin,
+ destination: fav.combi_routes.destination,
+ coordinate: {
+ latitude: parseFloat(fav.combi_routes.origin_latitude),
+ longitude: parseFloat(fav.combi_routes.origin_longitude),
+ },
+ }))
+ );
+ }
+ } catch (err) {
+ console.error('Fetch favorites error:', err);
+ Alert.alert('Error', 'An unexpected error occurred');
+ setFavoriteRoutes([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchFavorites();
+ }, [])
);
+ useEffect(() => {
+ animations.length = favoriteRoutes.length;
+ for (let i = 0; i < favoriteRoutes.length; i++) {
+ animations[i] = new Animated.Value(COLLAPSED_HEIGHT);
+ }
+ if (favoriteRoutes.length > 0) {
+ setExpandedId(favoriteRoutes[0].id);
+ Animated.spring(animations[0], {
+ toValue: EXPANDED_HEIGHT,
+ tension: 40,
+ friction: 8,
+ useNativeDriver: false,
+ }).start();
+ }
+ }, [favoriteRoutes.length]);
+
+ const handlePress = (idx, id) => {
+ const newExpandedId = expandedId === id ? null : id;
+ setExpandedId(newExpandedId);
+
+ favoriteRoutes.forEach((_, i) => {
+ Animated.spring(animations[i], {
+ toValue: i === idx && newExpandedId !== null ? EXPANDED_HEIGHT : COLLAPSED_HEIGHT,
+ tension: 40,
+ friction: 8,
+ useNativeDriver: false,
+ }).start();
+ });
+ };
+
+ const handleDeleteFavorite = async (routeId) => {
+ try {
+ const { data: { user } } = await supabase.auth.getUser();
+ if (!user) return;
+
+ const { error } = await supabase
+ .from('user_favorite_routes')
+ .delete()
+ .eq('user_id', user.id)
+ .eq('route_id', routeId);
+
+ if (error) {
+ console.error('Delete favorite error:', error);
+ Alert.alert('Error', 'Could not remove from favorites');
+ } else {
+ setFavoriteRoutes(favoriteRoutes.filter(route => route.id !== routeId));
+ setPopup({ visible: true, message: 'Removed from favorites!' });
+ notifyFavoriteChanged(); // <-- notify HomeScreen
+ }
+ } catch (err) {
+ console.error('Delete favorite error:', err);
+ Alert.alert('Error', 'An unexpected error occurred');
+ }
+ };
+
+ // Example: call this when you add a favorite elsewhere
+ const handleAddFavorite = () => {
+ setPopup({ visible: true, message: 'Added to favorites!' });
+ notifyFavoriteChanged(); // <-- notify HomeScreen
+ };
+
return (
-
- Favorites Screen
- item.id}
- numColumns={2}
- renderItem={renderItem}
- contentContainerStyle={styles.listContent}
- />
-
+
+
+
+
+ Favorites
+
+
+ setPopup({ ...popup, visible: false })}
+ />
+ {loading ? (
+
+ ) : (
+
+ {favoriteRoutes.length === 0 ? (
+
+ No favorites yet.
+
+ ) : favoriteRoutes.map((item, idx) => {
+ const isExpanded = expandedId === item.id;
+ return (
+
+
+
+
+
+
+ {item.name}
+ handleDeleteFavorite(item.id)} style={{ marginRight: 10 }}>
+
+
+ handlePress(idx, item.id)} style={{ marginLeft: 22 }}>
+
+
+
+
+
+ From:
+ {item.origin}
+
+
+
+ To:
+ {item.destination}
+
+ {isExpanded && (
+
+
+
+ navigation.navigate('MapViewScreen', { coordinate: item.coordinate })}
+ >
+ Show Route
+
+
+
+
+ {item.origin} → {item.destination}
+
+
+
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
);
};
+const popupStyles = StyleSheet.create({
+ popup: {
+ position: 'absolute',
+ top: 60,
+ alignSelf: 'center',
+ backgroundColor: '#018abe',
+ borderRadius: 12,
+ paddingVertical: 12,
+ paddingHorizontal: 32,
+ zIndex: 100,
+ shadowColor: '#000',
+ shadowOpacity: 0.18,
+ shadowRadius: 8,
+ shadowOffset: { width: 0, height: 4 },
+ elevation: 8,
+ },
+ popupText: {
+ color: '#fff',
+ fontWeight: 'bold',
+ fontSize: 16,
+ textAlign: 'center',
+ letterSpacing: 0.2,
+ },
+});
+
const styles = StyleSheet.create({
container: {
flex: 1,
+ backgroundColor: 'transparent',
+ paddingTop: 0,
+ },
+ scrollContainer: {
+ paddingVertical: 0,
+ paddingTop: 30,
+ paddingBottom: 30,
+ },
+ card: {
+ backgroundColor: 'white',
+ marginHorizontal: 16,
+ marginBottom: 18,
+ borderRadius: 16,
+ padding: 18,
+ shadowColor: '#000',
+ shadowOpacity: 0.08,
+ shadowRadius: 8,
+ elevation: 2,
+ zIndex: 2,
+ width: width - 32,
+ alignSelf: 'center',
+ },
+ firstCard: {
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ },
+ lastCard: {
+ borderBottomLeftRadius: 16,
+ borderBottomRightRadius: 16,
+ },
+ cardCollapsed: {
+ opacity: 0.7,
+ },
+ touchArea: {
+ flex: 1,
+ padding: 0,
justifyContent: 'center',
+ },
+ headerRow: {
+ flexDirection: 'row',
alignItems: 'center',
- backgroundColor: '#dce2ef',
- padding: 20,
+ marginBottom: 10,
},
- text: {
- fontSize: 24,
+ busIconContainer: {
+ width: 44,
+ height: 44,
+ borderRadius: 12,
+ backgroundColor: '#e6f2fa',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 10,
+ },
+ nameText: {
+ fontSize: 20,
fontWeight: 'bold',
- marginBottom: 20,
+ color: '#018abe',
+ flexShrink: 1,
},
- card: {
- width: '48%',
- aspectRatio: 1,
- margin: 5,
- padding: 10,
- borderWidth: 1,
- borderColor: 'gray',
- borderRadius: 5,
- justifyContent: 'center',
+ routeDetailsRow: {
+ flexDirection: 'row',
alignItems: 'center',
+ marginBottom: 2,
+ marginLeft: 4,
},
- cardText: {
- fontSize: 16,
+ detailLabel: {
+ fontWeight: 'bold',
+ color: '#555',
+ marginRight: 4,
+ fontSize: 15,
},
- listContent: {
- justifyContent: 'center',
+ detailValue: {
+ color: '#333',
+ fontSize: 15,
+ flexShrink: 1,
+ },
+ expandedContent: {
+ marginTop: 12,
+ },
+ mapContainer: {
+ height: 150,
+ marginBottom: 10,
+ borderRadius: 10,
+ overflow: 'hidden',
+ },
+ map: {
+ flex: 1,
+ borderRadius: 10,
+ },
+ routeButton: {
+ position: 'absolute',
+ bottom: 10,
+ right: 10,
+ backgroundColor: '#2d9cdb',
+ padding: 8,
+ borderRadius: 5,
+ },
+ routeButtonText: {
+ color: '#fff',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ extraRow: {
+ flexDirection: 'row',
alignItems: 'center',
- padding: 10,
+ justifyContent: 'space-between',
+ marginTop: 8,
+ },
+ extraLabel: {
+ fontSize: 14,
+ color: '#888',
},
});
diff --git a/screens/HomeScreen.js b/screens/HomeScreen.js
index 88a7a43..34d43dc 100644
--- a/screens/HomeScreen.js
+++ b/screens/HomeScreen.js
@@ -1,29 +1,920 @@
-import React from 'react';
-import { View, Text, StyleSheet, Dimensions } from 'react-native';
+import React, { useState, useEffect, useRef } from 'react';
+import { Image, View, TextInput, FlatList, TouchableOpacity, Text, StyleSheet, Alert, ActivityIndicator, ImageBackground, Animated, PanResponder, Dimensions } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { supabase } from '../lib/supabase';
+import * as Location from 'expo-location';
+import haversine from 'haversine-distance';
+import UserHeader from '../components/UserHeader';
+import { decodePolyline } from '../utils';
+import { debounce } from 'lodash';
+import { Ionicons } from '@expo/vector-icons';
+import { useFavorite } from '../contexts/FavoriteContext';
+
+// --- Custom Popup Component (same as FavoritesScreen) ---
+const AnimatedPopup = ({ visible, message, onHide }) => {
+ const opacity = useRef(new Animated.Value(0)).current;
+ const translateY = useRef(new Animated.Value(40)).current;
+
+ useEffect(() => {
+ if (visible) {
+ Animated.parallel([
+ Animated.timing(opacity, {
+ toValue: 1,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ Animated.spring(translateY, {
+ toValue: 0,
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ // Auto-hide after 1.8s
+ const timer = setTimeout(() => {
+ Animated.parallel([
+ Animated.timing(opacity, {
+ toValue: 0,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ Animated.timing(translateY, {
+ toValue: 40,
+ duration: 250,
+ useNativeDriver: true,
+ }),
+ ]).start(() => {
+ onHide && onHide();
+ });
+ }, 1800);
+
+ return () => clearTimeout(timer);
+ }
+ }, [visible]);
+
+ if (!visible) return null;
-const HomeScreen = (navigation) => {
return (
-
-
+
+ {message}
+
);
};
-const styles = StyleSheet.create({
- safeArea: {
- flex: 1,
- backgroundColor: '#dce2ef',
+const BOTTOM_SHEET_MIN_HEIGHT = 80;
+const BOTTOM_SHEET_MAX_HEIGHT = Dimensions.get('window').height * 0.55;
+
+const HomeScreen = () => {
+ const [from, setFrom] = useState('');
+ const [to, setTo] = useState('');
+ const [stops, setStops] = useState([]);
+ const [filteredFromStops, setFilteredFromStops] = useState([]);
+ const [filteredToStops, setFilteredToStops] = useState([]);
+ const navigation = useNavigation();
+ const [fromStopData, setFromStopData] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [allStopsAndTerminals, setAllStopsAndTerminals] = useState([]);
+ const [currentRouteId, setCurrentRouteId] = useState(null);
+ const [isFavorite, setIsFavorite] = useState(false);
+ const [favoriteLoading, setFavoriteLoading] = useState(false);
+
+ const [popup, setPopup] = useState({ visible: false, message: '' });
+
+ const { favoriteChanged, notifyFavoriteChanged } = useFavorite();
+
+ // --- Bottom Sheet State ---
+ const [sheetVisible, setSheetVisible] = useState(false);
+ const sheetAnim = useRef(new Animated.Value(BOTTOM_SHEET_MIN_HEIGHT)).current;
+ const lastSheetHeight = useRef(BOTTOM_SHEET_MIN_HEIGHT);
+
+ // Animated height for the sheet
+ const animatedSheetHeight = Animated.add(sheetAnim, new Animated.Value(lastSheetHeight.current)).interpolate({
+ inputRange: [BOTTOM_SHEET_MIN_HEIGHT, BOTTOM_SHEET_MAX_HEIGHT],
+ outputRange: [BOTTOM_SHEET_MIN_HEIGHT, BOTTOM_SHEET_MAX_HEIGHT],
+ extrapolate: 'clamp',
+ });
+
+ const panResponder = useRef(
+ PanResponder.create({
+ onMoveShouldSetPanResponder: (_, gestureState) => Math.abs(gestureState.dy) > 10,
+ onPanResponderGrant: () => {
+ sheetAnim.setOffset(lastSheetHeight.current);
+ sheetAnim.setValue(0);
+ },
+ onPanResponderMove: (_, gestureState) => {
+ let newHeight = lastSheetHeight.current - gestureState.dy;
+ newHeight = Math.max(BOTTOM_SHEET_MIN_HEIGHT, Math.min(BOTTOM_SHEET_MAX_HEIGHT, newHeight));
+ sheetAnim.setValue(newHeight - lastSheetHeight.current);
+ },
+ onPanResponderRelease: (_, gestureState) => {
+ sheetAnim.flattenOffset();
+ let newHeight = lastSheetHeight.current - gestureState.dy;
+ if (gestureState.dy < -50 || newHeight > (BOTTOM_SHEET_MIN_HEIGHT + BOTTOM_SHEET_MAX_HEIGHT) / 2) {
+ // Open
+ Animated.spring(sheetAnim, {
+ toValue: BOTTOM_SHEET_MAX_HEIGHT - lastSheetHeight.current,
+ useNativeDriver: false,
+ }).start(() => {
+ lastSheetHeight.current = BOTTOM_SHEET_MAX_HEIGHT;
+ sheetAnim.setValue(0);
+ setSheetVisible(true);
+ });
+ } else {
+ // Close
+ Animated.spring(sheetAnim, {
+ toValue: BOTTOM_SHEET_MIN_HEIGHT - lastSheetHeight.current,
+ useNativeDriver: false,
+ }).start(() => {
+ lastSheetHeight.current = BOTTOM_SHEET_MIN_HEIGHT;
+ sheetAnim.setValue(0);
+ setSheetVisible(false);
+ });
+ }
+ },
+ })
+ ).current;
+
+ // Fetch all stops and terminals ONCE for smooth filtering
+ useEffect(() => {
+ const fetchAll = async () => {
+ try {
+ const { data: stopsData, error: stopsError } = await supabase.from('stops').select('*');
+ const { data: routesData, error: routesError } = await supabase.from('combi_routes').select('*');
+ if (stopsError || routesError) {
+ Alert.alert('Error', 'Failed to fetch stops or routes.');
+ return;
+ }
+ setStops(stopsData);
+
+ // Create terminal entries
+ const terminals = [];
+ routesData.forEach(route => {
+ terminals.push({
+ id: `origin-${route.id}`,
+ name: route.origin,
+ latitude: route.origin_latitude,
+ longitude: route.origin_longitude,
+ route_id: route.id,
+ stop_order: 0,
+ isTerminal: true
+ });
+ terminals.push({
+ id: `destination-${route.id}`,
+ name: route.destination,
+ latitude: route.destination_latitude,
+ longitude: route.destination_longitude,
+ route_id: route.id,
+ stop_order: 999,
+ isTerminal: true
+ });
+ });
+ setAllStopsAndTerminals([...stopsData, ...terminals]);
+ } catch (error) {
+ Alert.alert('Error', 'Failed to fetch stops and terminals.');
+ }
+ };
+ fetchAll();
+ }, []);
+
+ // Keep currentRouteId in sync with from/to
+ useEffect(() => {
+ const updateRouteId = async () => {
+ if (!from || !to) {
+ setCurrentRouteId(null);
+ setIsFavorite(false);
+ return;
+ }
+ const { data, error } = await supabase
+ .from('combi_routes')
+ .select('id')
+ .eq('origin', from)
+ .eq('destination', to)
+ .single();
+ if (data && data.id) {
+ setCurrentRouteId(data.id);
+ const { data: { user } } = await supabase.auth.getUser();
+ if (user) {
+ const { data: favData } = await supabase
+ .from('user_favorite_routes')
+ .select('id')
+ .eq('user_id', user.id)
+ .eq('route_id', data.id)
+ .single();
+ setIsFavorite(!!favData);
+ }
+ } else {
+ setCurrentRouteId(null);
+ setIsFavorite(false);
+ }
+ };
+ updateRouteId();
+ // eslint-disable-next-line
+ }, [from, to]);
+
+ // Re-check favorite status when favoriteChanged or currentRouteId changes
+ useEffect(() => {
+ const checkIfFavorite = async (routeId) => {
+ const { data: { user } } = await supabase.auth.getUser();
+ if (user && routeId) {
+ const { data: favData } = await supabase
+ .from('user_favorite_routes')
+ .select('id')
+ .eq('user_id', user.id)
+ .eq('route_id', routeId)
+ .single();
+ setIsFavorite(!!favData);
+ }
+ };
+ if (currentRouteId) {
+ checkIfFavorite(currentRouteId);
+ }
+ }, [favoriteChanged, currentRouteId]);
+
+ // Debounced filter function for smooth UX
+ const debouncedFilter = useRef(
+ debounce((text, type, fromStopDataRef, allStopsAndTerminalsRef) => {
+ if (!text.trim()) {
+ type === 'from' ? setFilteredFromStops([]) : setFilteredToStops([]);
+ return;
+ }
+ let filtered = allStopsAndTerminalsRef.filter(stop =>
+ stop.name.toLowerCase().includes(text.toLowerCase())
+ );
+ // If filtering "to", restrict to same route as "from"
+ if (type === 'to' && fromStopDataRef) {
+ filtered = filtered.filter(stop => stop.route_id === fromStopDataRef.route_id);
+ }
+ if (type === 'from') {
+ setFilteredFromStops(filtered);
+ } else {
+ setFilteredToStops(filtered);
+ }
+ }, 200)
+ ).current;
+
+ const handleInputChange = (text, type) => {
+ if (type === 'from') setFrom(text);
+ else setTo(text);
+ debouncedFilter(
+ text,
+ type,
+ fromStopData,
+ allStopsAndTerminals
+ );
+ };
+
+ const handleSelectStop = (stop, type) => {
+ if (type === 'from') {
+ setFrom(stop.name);
+ setFromStopData(stop);
+ setFilteredFromStops([]);
+ setTo('');
+ setFilteredToStops([]);
+ } else {
+ setTo(stop.name);
+ setFilteredToStops([]);
+ }
+ };
+
+ const clearFrom = () => {
+ setFrom('');
+ setFilteredFromStops([]);
+ setFromStopData(null);
+ setTo('');
+ setFilteredToStops([]);
+ };
+ const clearTo = () => {
+ setTo('');
+ setFilteredToStops([]);
+ };
+
+ const handleSwapStops = async () => {
+ const tempFrom = from;
+ const tempTo = to;
+
+ // Find the stop data for the "To" stop in allStopsAndTerminals
+ const toStopData = allStopsAndTerminals.find(stop => stop.name === tempTo);
+
+ setFrom(tempTo);
+ setTo(tempFrom);
+ setFromStopData(toStopData || null);
+ setFilteredFromStops([]);
+ setFilteredToStops([]);
+ };
+
+ const handleSearch = async () => {
+ if (!from || !to) {
+ Alert.alert('Error', 'Please enter both "From" and "To" stops.');
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ // 1. Find the correct route_id for the selected direction
+ let routeData, routeError;
+ // If both from and to are terminals, use combi_routes to find the matching direction
+ if (
+ allStopsAndTerminals.find(stop => stop.name === from && stop.isTerminal) &&
+ allStopsAndTerminals.find(stop => stop.name === to && stop.isTerminal)
+ ) {
+ const { data, error } = await supabase
+ .from('combi_routes')
+ .select('*')
+ .eq('origin', from)
+ .eq('destination', to)
+ .single();
+ routeData = data;
+ routeError = error;
+ } else {
+ // Otherwise, fallback to using the route_id of the from stop
+ const fromStopDataObj = allStopsAndTerminals.find(stop => stop.name === from);
+ if (!fromStopDataObj) {
+ Alert.alert('Error', 'Invalid "From" stop.');
+ setIsLoading(false);
+ return;
+ }
+ const { data, error } = await supabase
+ .from('combi_routes')
+ .select('*')
+ .eq('id', fromStopDataObj.route_id)
+ .single();
+ routeData = data;
+ routeError = error;
+ }
+
+ if (routeError || !routeData) {
+ Alert.alert('Error', 'Could not find a route for the selected direction.');
+ setIsLoading(false);
+ return;
+ }
+
+ // Set the current route ID
+ setCurrentRouteId(routeData.id);
+
+ // Check if this route is already a favorite for the current user
+ const { data: { user } } = await supabase.auth.getUser();
+ if (user) {
+ const { data: favData } = await supabase
+ .from('user_favorite_routes')
+ .select('id')
+ .eq('user_id', user.id)
+ .eq('route_id', routeData.id)
+ .single();
+ setIsFavorite(!!favData);
+ }
+
+ // 2. Now fetch stops for this route_id
+ const { data: routeWaypoints, error: routeWaypointsError } = await supabase
+ .from('stops')
+ .select('latitude, longitude, stop_order, name, route_id')
+ .eq('route_id', routeData.id)
+ .order('stop_order', { ascending: true });
+
+ if (routeWaypointsError) {
+ Alert.alert('Error', 'Failed to fetch route waypoints.');
+ setIsLoading(false);
+ return;
+ }
+
+ // 3. Build the full stops+terminals array for this direction
+ const allStopsWithTerminals = [
+ {
+ latitude: parseFloat(routeData.origin_latitude),
+ longitude: parseFloat(routeData.origin_longitude),
+ name: routeData.origin,
+ stop_order: 0,
+ isTerminal: true,
+ route_id: routeData.id,
+ },
+ ...routeWaypoints,
+ {
+ latitude: parseFloat(routeData.destination_latitude),
+ longitude: parseFloat(routeData.destination_longitude),
+ name: routeData.destination,
+ stop_order: 999,
+ isTerminal: true,
+ route_id: routeData.id,
+ }
+ ];
+
+ // 4. Find indexes for from/to in this direction (match both name and route_id for uniqueness)
+ const fromIdx = allStopsWithTerminals.findIndex(
+ stop => stop.name === from && stop.route_id === routeData.id
+ );
+ const toIdx = allStopsWithTerminals.findIndex(
+ stop => stop.name === to && stop.route_id === routeData.id
+ );
+
+ if (fromIdx === -1 || toIdx === -1) {
+ Alert.alert('Error', 'Could not find both stops in the route.');
+ setIsLoading(false);
+ return;
+ }
+
+ // 5. Slice the waypoints in the correct direction
+ let completeWaypoints;
+ if (fromIdx <= toIdx) {
+ completeWaypoints = allStopsWithTerminals.slice(fromIdx, toIdx + 1);
+ } else {
+ completeWaypoints = allStopsWithTerminals.slice(toIdx, fromIdx + 1).reverse();
+ }
+
+ if (!completeWaypoints || completeWaypoints.length < 2) {
+ Alert.alert('Error', 'Insufficient waypoints to create a route.');
+ setIsLoading(false);
+ return;
+ }
+
+ // 6. Prepare origin/destination objects for navigation
+ const origin = {
+ latitude: parseFloat(completeWaypoints[0].latitude),
+ longitude: parseFloat(completeWaypoints[0].longitude)
+ };
+ const destination = {
+ latitude: parseFloat(completeWaypoints[completeWaypoints.length - 1].latitude),
+ longitude: parseFloat(completeWaypoints[completeWaypoints.length - 1].longitude)
+ };
+
+ // Replace the existing waypoints generation code
+ const getDetailedRoute = async (waypoints) => {
+ try {
+ if (!waypoints || waypoints.length < 2) {
+ Alert.alert("Error", "Insufficient waypoints to create a route.");
+ return null;
+ }
+
+ // Validate waypoints
+ for (const wp of waypoints) {
+ if (!wp || typeof wp.latitude !== 'number' || typeof wp.longitude !== 'number') {
+ Alert.alert("Error", "Invalid waypoint data.");
+ return null;
+ }
+ }
+
+ const waypointsStr = waypoints
+ .map(wp => `${wp.latitude},${wp.longitude}`)
+ .join('|');
+
+ const directionsUrl = `https://maps.gomaps.pro/maps/api/directions/json?` +
+ `origin=${waypoints[0].latitude},${waypoints[0].longitude}&` +
+ `destination=${waypoints[waypoints.length-1].latitude},${waypoints[waypoints.length-1].longitude}&` +
+ `waypoints=optimize:false|${waypointsStr}&` +
+ `mode=driving&` +
+ `alternatives=true&` +
+ `avoid=highways|ferries&` +
+ `key=AlzaSy0csWCFtrxT-TmMw4adcHN41jNcy0mdvdf`;
+
+ const response = await fetch(directionsUrl);
+ const directionsData = await response.json();
+
+ if (directionsData.status === 'OK') {
+ const routes = directionsData.routes;
+ let shortestRoute = routes[0];
+ let shortestDistance = Number.MAX_VALUE;
+
+ routes.forEach(route => {
+ const distance = route.legs.reduce((total, leg) => total + leg.distance.value, 0);
+ if (distance < shortestDistance) {
+ shortestDistance = distance;
+ shortestRoute = route;
+ }
+ });
+
+ return decodePolyline(shortestRoute.overview_polyline.points);
+ } else {
+ Alert.alert('No Route Found', 'No route could be found for the selected stops. Please try different stops.');
+ return null;
+ }
+ } catch (error) {
+ Alert.alert('Error', 'An error occurred while fetching the route.');
+ return null;
+ }
+ };
+
+ const allWaypoints = await getDetailedRoute(completeWaypoints);
+
+ if (!allWaypoints || allWaypoints.length === 0) {
+ setIsLoading(false);
+ return;
+ }
+
+ navigation.navigate('MainTabNavigator', {
+ screen: 'MapViewScreen',
+ params: {
+ origin,
+ destination,
+ waypoints: allWaypoints,
+ routeWaypoints: completeWaypoints,
+ route_name: routeData?.route_name || routeData?.name,
+ },
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleUseMyLocation = async () => {
+ const { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
+ if (status !== 'granted') {
+ if (canAskAgain) {
+ const { status: requestStatus } = await Location.requestForegroundPermissionsAsync();
+ if (requestStatus !== 'granted') {
+ Alert.alert('Permission Denied', 'Location permission is required to use this feature.');
+ return;
+ }
+ } else {
+ Alert.alert('Permission Denied', 'Location permission is not enabled. Please enable it in your device settings.');
+ return;
+ }
+ }
+
+ try {
+ const location = await Location.getCurrentPositionAsync({});
+ const { latitude, longitude } = location.coords;
+
+ let nearestStop = null;
+ let minDistance = Infinity;
+
+ stops.forEach((stop) => {
+ const stopLat = Number(stop.latitude);
+ const stopLng = Number(stop.longitude);
+ const distance = haversine(
+ { latitude: Number(latitude), longitude: Number(longitude) },
+ { latitude: stopLat, longitude: stopLng }
+ );
+ if (distance < minDistance) {
+ minDistance = distance;
+ nearestStop = stop;
+ }
+ });
+
+ if (nearestStop) {
+ setFrom(nearestStop.name);
+ setFromStopData(nearestStop);
+ setFilteredFromStops([]);
+ setTo('');
+ setFilteredToStops([]);
+ Alert.alert(
+ 'Nearest Stop',
+ `Your location:\nLat: ${latitude}\nLng: ${longitude}\n\nNearest stop: ${nearestStop.name}\nLat: ${nearestStop.latitude}\nLng: ${nearestStop.longitude}\n\nDistance: ${(minDistance / 1000).toFixed(2)} km`
+ );
+ } else {
+ Alert.alert('No Stops Found', 'Could not find any stops nearby.');
+ }
+ } catch (error) {
+ Alert.alert('Error', 'Could not get your current location.');
+ }
+ };
+
+ const handleFavoritePress = async () => {
+ setFavoriteLoading(true);
+ try {
+ const { data: { user } } = await supabase.auth.getUser();
+
+ if (!user) {
+ setPopup({ visible: true, message: 'You must be logged in to save favorites.' });
+ setFavoriteLoading(false);
+ return;
+ }
+
+ if (isFavorite) {
+ // Remove favorite
+ const { error } = await supabase
+ .from('user_favorite_routes')
+ .delete()
+ .eq('user_id', user.id)
+ .eq('route_id', currentRouteId);
+
+ if (error) {
+ console.error('Delete favorite error:', error);
+ setPopup({ visible: true, message: 'Could not remove from favorites.' });
+ } else {
+ setIsFavorite(false);
+ notifyFavoriteChanged();
+ setPopup({ visible: true, message: 'Route removed from favorites!' });
+ }
+ } else {
+ // Add favorite - check if it already exists first
+ const { data: existingFav } = await supabase
+ .from('user_favorite_routes')
+ .select('id')
+ .eq('user_id', user.id)
+ .eq('route_id', currentRouteId);
+
+ if (existingFav && existingFav.length > 0) {
+ setPopup({ visible: true, message: 'This route is already in your favorites.' });
+ } else {
+ // Insert new favorite
+ const { error } = await supabase
+ .from('user_favorite_routes')
+ .insert([{ user_id: user.id, route_id: currentRouteId }]);
+
+ if (error) {
+ console.error('Add favorite error:', error);
+ setPopup({ visible: true, message: 'Could not add to favorites.' });
+ } else {
+ setIsFavorite(true);
+ notifyFavoriteChanged();
+ setPopup({ visible: true, message: 'Route added to favorites!' });
+ }
+ }
+ }
+ } catch (err) {
+ console.error('Favorite action error:', err);
+ setPopup({ visible: true, message: 'An unexpected error occurred.' });
+ } finally {
+ setFavoriteLoading(false);
+ }
+ };
+
+ return (
+
+ {/* --- Logo and App Name Row at the very top --- */}
+
+
+ LoetoLink
+
+
+
+ setPopup({ ...popup, visible: false })}
+ />
+
+ {/* Move all main controls up, just below the logo/app name */}
+ navigation.navigate('Profile')} />
+
+
+
+ handleInputChange(text, 'from')}
+ />
+ {from.length > 0 && (
+
+ ✕
+
+ )}
+
+ {filteredFromStops.length > 0 && (
+ item.id.toString()}
+ renderItem={({ item }) => (
+ handleSelectStop(item, 'from')}
+ >
+ {item.name}
+
+ )}
+ />
+ )}
+
+
+
+
+ handleInputChange(text, 'to')}
+ editable={from !== ''}
+ />
+ {to.length > 0 && (
+
+ ✕
+
+ )}
+
+ {filteredToStops.length > 0 && (
+ item.id.toString()}
+ renderItem={({ item }) => (
+ handleSelectStop(item, 'to')}
+ >
+ {item.name}
+
+ )}
+ />
+ )}
+
+
+
+ {isLoading ? (
+
+ ) : (
+ Search
+ )}
+
+
+ Use My Location
+
+ {currentRouteId && (
+
+
+
+ {favoriteLoading
+ ? 'Saving...'
+ : isFavorite
+ ? 'Remove from Favorites'
+ : 'Add to Favorites'}
+
+
+
+
+ )}
+
+
+ {/* --- Bottom Sheet --- */}
+
+ {/* Drag Handle */}
+
+
+
+ {/* Header */}
+
+ Places To Visit
+
+ {/* Content */}
+
+
+
+
+
+ );
+};
+
+const popupStyles = StyleSheet.create({
+ popup: {
+ position: 'absolute',
+ top: 60,
+ alignSelf: 'center',
+ backgroundColor: '#018abe',
+ borderRadius: 12,
+ paddingVertical: 12,
+ paddingHorizontal: 32,
+ zIndex: 100,
+ shadowColor: '#000',
+ shadowOpacity: 0.18,
+ shadowRadius: 8,
+ shadowOffset: { width: 0, height: 4 },
+ elevation: 8,
+ },
+ popupText: {
+ color: '#fff',
+ fontWeight: 'bold',
+ fontSize: 16,
+ textAlign: 'center',
+ letterSpacing: 0.2,
},
+});
+const styles = StyleSheet.create({
container: {
flex: 1,
- justifyContent: 'center',
+ padding: 20,
+ backgroundColor: '#f9f9f9',
+ },
+ logoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 10,
+ marginTop: 10,
+ },
+ logo: {
+ width: 48,
+ height: 48,
+ borderRadius: 12,
+ marginRight: 10,
+ },
+ appName: {
+ fontSize: 32,
+ fontFamily: 'jgs',
+ color: '#212842',
+ fontWeight: 'bold',
+ letterSpacing: 1,
+ },
+ inputContainer: {
+ marginBottom: 15,
+ },
+ input: {
+ height: 50,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 15,
+ paddingHorizontal: 15,
+ backgroundColor: 'white',
+ },
+ dropdown: {
+ maxHeight: 150,
+ backgroundColor: 'white',
+ borderRadius: 10,
+ marginBottom: 10,
+ },
+ dropdownItem: {
+ padding: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: '#ccc',
+ },
+ button: {
+ backgroundColor: '#018abe',
+ padding: 15,
+ borderRadius: 15,
alignItems: 'center',
- backgroundColor: '#dce2ef',
- width: Dimensions.get('window').width,
- height: Dimensions.get('window').height,
+ marginBottom: 10,
+ },
+ buttonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ swapButton: {
+ backgroundColor: '#018abe',
+ width: 50,
+ height: 50,
+ borderRadius: 25,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 15,
+ alignSelf: 'center',
},
- text: {
- fontSize: 24,
+ swapButtonText: {
+ color: 'white',
+ fontSize: 25,
fontWeight: 'bold',
},
});
diff --git a/screens/LoginScreen.js b/screens/LoginScreen.js
index d5b7ca5..768c7a5 100644
--- a/screens/LoginScreen.js
+++ b/screens/LoginScreen.js
@@ -1,11 +1,158 @@
-import React, { useEffect, useRef } from 'react';
-import { Image, Animated } from 'react-native';
-import { View, Text, TextInput, StyleSheet, TouchableOpacity } from 'react-native';import { useNavigation } from '@react-navigation/native';
+import React, { useState, useEffect, useRef } from 'react';
+import { Alert, Animated, Image, StyleSheet, Text, TextInput, TouchableOpacity, View, ImageBackground } from 'react-native';
+import { supabase } from '../lib/supabase'; // Adjust the import path as necessary
+import { useNavigation } from '@react-navigation/native';
+import * as WebBrowser from 'expo-web-browser';
+import * as AuthSession from 'expo-auth-session';
+import * as AppleAuthentication from 'expo-apple-authentication';
+import { Platform } from 'react-native';
+import CustomPopup from '../components/CustomPopup';
+
+//working perfectly fine
const LoginScreen = () => {
const navigation = useNavigation();
const fadeAnim = useRef(new Animated.Value(0)).current;
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [popup, setPopup] = useState({ visible: false, title: '', message: '', onConfirm: null });
+ const [forgotPopup, setForgotPopup] = useState(false);
+ const [resetEmail, setResetEmail] = useState('');
+ const [resetCode, setResetCode] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [step, setStep] = useState(1); // 1: email, 2: code+password
+
+ // Function to handle user login
+ const signInWithEmail = async () => {
+ const { error } = await supabase.auth.signInWithPassword({ email, password });
+ if (error) {
+ setPopup({
+ visible: true,
+ title: 'Login Failed',
+ message: error.message,
+ onConfirm: () => setPopup({ ...popup, visible: false }),
+ });
+ } else {
+ setPopup({
+ visible: true,
+ title: 'Login Successful',
+ message: 'Welcome back!',
+ onConfirm: () => {
+ setPopup({ ...popup, visible: false });
+ navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator' }] });
+ },
+ });
+ }
+ };
+
+ // Function to handle user registration
+ const signUpWithEmail = async () => {
+ const { data, error } = await supabase.auth.signUp({ email, password });
+ if (error) {
+ Alert.alert('Registration Failed', error.message);
+ } else {
+ Alert.alert('Registration Successful', 'Please check your email for verification.');
+ }
+ };
+
+ // Google Sign-In function
+ const signInWithGoogle = async () => {
+ const redirectUrl = AuthSession.makeRedirectUri({ useProxy: false });
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider: 'google',
+ options: { redirectTo: redirectUrl }
+ });
+
+ if (error) {
+ Alert.alert('Google Sign-In failed', error.message);
+ return;
+ }
+ if (data?.url) {
+ const result = await WebBrowser.openAuthSessionAsync(data.url, redirectUrl);
+ // After OAuth, Supabase will handle the session.
+ // check if user is logged in and navigate:
+ const { data: userData } = await supabase.auth.getUser();
+ if (userData?.user) {
+ navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator' }] });
+ }
+ }
+ };
+
+ // Apple Sign-In function (iOS only)
+ const signInWithApple = async () => {
+ if (Platform.OS !== 'ios') {
+ Alert.alert('Apple Sign-In is only available on iOS.');
+ return;
+ }
+ const redirectUrl = AuthSession.makeRedirectUri({ useProxy: false });
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider: 'apple',
+ options: { redirectTo: redirectUrl }
+ });
+
+ if (error) {
+ Alert.alert('Apple Sign-In failed', error.message);
+ return;
+ }
+ if (data?.url) {
+ const result = await WebBrowser.openAuthSessionAsync(data.url, redirectUrl);
+ const { data: userData } = await supabase.auth.getUser();
+ if (userData?.user) {
+ navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator' }] });
+ }
+ }
+ };
+
+ // Send reset email
+ const handleSendResetEmail = async () => {
+ const { error } = await supabase.auth.resetPasswordForEmail(resetEmail);
+ if (error) {
+ Alert.alert('Error', error.message);
+ } else {
+ setStep(2);
+ Alert.alert('Check your email', 'Enter the code sent to your email and your new password.');
+ }
+ };
+
+ // Verify code and set new password
+ const handleResetPassword = async () => {
+ if (newPassword !== confirmPassword) {
+ Alert.alert('Error', 'Passwords do not match.');
+ return;
+ }
+ const { error } = await supabase.auth.verifyOtp({
+ email: resetEmail,
+ token: resetCode,
+ type: 'recovery',
+ options: { password: newPassword }
+ });
+ if (error) {
+ Alert.alert('Error', error.message);
+ } else {
+ setForgotPopup(false);
+ setStep(1);
+ setResetEmail('');
+ setResetCode('');
+ setNewPassword('');
+ setConfirmPassword('');
+ Alert.alert('Success', 'Password reset successful. Please sign in.');
+ }
+ };
+
+ // Listen for auth state changes (handles OAuth redirect)
+ /*useEffect(() => {
+ const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
+ if (session && session.user) {
+ navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator' }] });
+ }
+ });
+ return () => {
+ authListener.subscription.unsubscribe();
+ };
+ }, []); */
+
useEffect(() => {
const fadeIn = Animated.timing(fadeAnim, {
toValue: 1,
@@ -18,96 +165,230 @@ const LoginScreen = () => {
useNativeDriver: true,
});
- Animated.loop(
- Animated.sequence([fadeIn, fadeOut])
- ).start();
+ Animated.loop(Animated.sequence([fadeIn, fadeOut])).start();
}, []);
return (
-
-
- LoetoLink
-
-
+
+
+ LoetoLink
+
+
+
- Welcome!
-
-
- Sign up with Apple
-
-
-
- Sign up with Google
-
- navigation.navigate('Login')}>
- I already have an account
-
-
-
- navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator', params: { screen: 'Map' } }], })}>
- Continue as Guest
-
-
-
- By continuing you confirm that you agree to our{' '}
- {}}>Terms of Service, {' '}
- {}}>Bus/Combi Policy and good behavior in the bus/combi.
-
-
+
+
+ Sign In
+
+
+ Sign Up
+
+ setForgotPopup(true)}>
+
+ Forgot Password?
+
+
+
+
+
+
+
+
+
+
+
+
+
+ navigation.reset({
+ index: 0,
+ routes: [{ name: 'MainTabNavigator', params: { screen: 'Map' } }],
+ })
+ }
+ >
+ Continue as Guest
+
+
+
+ By continuing you confirm that you agree to our{' '}
+ { }}>
+ Terms of Service
+
+ ,{' '}
+ { }}>
+ Bus/Combi Policy
+ {' '}
+ and good behavior in the bus/combi.
+
+ setPopup({ ...popup, visible: false })}
+ />
+
+ {/* Forgot Password Popup */}
+ {forgotPopup && (
+
+
+ {step === 1 ? (
+ <>
+ Reset Password
+
+
+ Send Reset Code
+
+ setForgotPopup(false)}>
+ Cancel
+
+ >
+ ) : (
+ <>
+ Enter Code & New Password
+
+
+
+
+ Reset Password
+
+ {
+ setStep(1);
+ setForgotPopup(false);
+ }}>
+ Cancel
+
+ >
+ )}
+
+
+ )}
+
+
);
};
const styles = StyleSheet.create({
+ backgroundImage: {
+ flex: 1,
+ resizeMode: 'cover', // or 'stretch'
+ width: '100%',
+ height: '100%',
+ justifyContent: 'center', // centers the content vertically
+ },
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
- backgroundColor: '#fff',
+ backgroundColor: '#transparent',
padding: 20,
},
+ headerContainer: {
+ alignItems: 'center', // Center items horizontally
+ marginBottom: 20, // Add some space below the logo
+ },
header: {
position: 'absolute',
- top: 60,
+ top: 20,
left: 20,
- alignSelf: 'flex-start',
+ flexDirection: 'row',
+ alignItems: 'center',
},
logoText: {
- fontSize: 24,
+ fontSize: 40,
fontWeight: 'bold',
+ textAlign: 'center',
+ marginBottom: 10,
+ fontFamily: 'Arial', // Change to a more attractive font
+ },
+ splashImage: {
+ width: 100,
+ height: 100,
+ resizeMode: 'contain',
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.5, // Darken the shadow
+ shadowRadius: 30, // Increase the shadow radius
+ elevation: 5,
+ backgroundColor: 'white',
+ borderRadius: 15, // Add a borderRadius for rounded corners
},
title: {
fontSize: 24,
fontWeight: 'bold',
- marginBottom: 40,
- },
- subtitle: {
- fontSize: 16,
- marginBottom: 40,
- },
- highlight: {
- fontWeight: 'bold',
+ marginBottom: 20,
},
- button: {
- backgroundColor: '#000',
- padding: 10,
- borderRadius: 25,
+ input: {
width: '100%',
- alignItems: 'center',
- justifyContent: 'center',
- marginTop: 10,
- marginBottom: 25,
+ padding: 15,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 25,
+ marginBottom: 15,
+ backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: {
width: 0,
@@ -117,14 +398,15 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
- appleButton: {
+ button: {
backgroundColor: '#000',
padding: 10,
borderRadius: 25,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
- marginBottom: 25,
+ marginTop: 10,
+ marginBottom: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
@@ -134,8 +416,8 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
- googleButton: {
- backgroundColor: '#fff',
+ newButton: {
+ backgroundColor: '#808080',
padding: 10,
borderRadius: 25,
width: '100%',
@@ -151,15 +433,26 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
- guestButton: {
- backgroundColor: '#f0f0f0',
+ buttonText: {
+ color: '#fff',
+ textAlign: 'center',
+ fontSize: 16,
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ marginBottom: 25,
+ },
+ appleButton: {
+ backgroundColor: '#000',
padding: 10,
borderRadius: 25,
- width: '100%',
- flexDirection: 'row',
+ width: '25%',
alignItems: 'center',
justifyContent: 'center',
- marginBottom: 25,
+ marginRight: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
@@ -169,32 +462,13 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
- buttonText: {
- color: '#fff',
- textAlign: 'center',
- fontSize: 16,
- },
- guestButtonText: {
- color: '#666',
- textAlign: 'center',
- fontSize: 16,
- marginRight: 10,
- },
- appleButtonText: {
- color: '#fff',
- textAlign: 'center',
- fontSize: 16,
- },
- googleButtonText: {
- color: '#000',
- textAlign: 'center',
- fontSize: 16,
- },
- secondaryButton: {
- backgroundColor: '#204ECF',
+ googleButton: {
+ backgroundColor: '#fff',
padding: 10,
borderRadius: 25,
- width: '100%',
+ width: '25%',
+ alignItems: 'center',
+ justifyContent: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
@@ -204,18 +478,20 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
- appleIcon: {
- width: 20,
- height: 20,
- marginRight: 171,
- marginBottom: -21,
- tintColor: '#fff',
+ guestButton: {
+ backgroundColor: '#f0f0f0',
+ padding: 10,
+ borderRadius: 25,
+ width: '100%',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 25,
},
- googleIcon: {
- width: 20,
- height: 20,
- marginRight: 171,
- marginBottom: -21,
+ guestButtonText: {
+ color: '#666',
+ textAlign: 'center',
+ fontSize: 16,
},
arrowIcon: {
width: 20,
@@ -232,7 +508,7 @@ const styles = StyleSheet.create({
link: {
textDecorationLine: 'underline',
color: 'blue',
- }
+ },
});
export default LoginScreen;
\ No newline at end of file
diff --git a/screens/MapViewScreen.js b/screens/MapViewScreen.js
index f0c3a30..f71ec6f 100644
--- a/screens/MapViewScreen.js
+++ b/screens/MapViewScreen.js
@@ -1,56 +1,505 @@
-import React from 'react';
-import { View, StyleSheet, Dimensions } from 'react-native';
-import MapView, { Marker } from 'react-native-maps';
-
-const taxiRanks = [
- {
- id: 'broadhurst',
- name: 'Broadhurst Taxi Rank',
- latitude: -24.6251926,
- longitude: 25.9368589,
- },
- {
- id: 'gamecity',
- name: 'Game City Taxi Rank',
- latitude: -24.6869378,
- longitude: 25.8770554,
- },
- {
- id: 'mainmall',
- name: 'Main Mall Taxi Rank',
- latitude: -24.6586851,
- longitude: 25.9021672,
- },
-];
+import React, { useState, useEffect, useRef } from 'react';
+import {
+ View,
+ StyleSheet,
+ Dimensions,
+ Text,
+ TouchableOpacity,
+ FlatList,
+ Animated,
+ PanResponder,
+} from 'react-native';
+import MapView, { Marker, Polyline } from 'react-native-maps';
+import * as Location from 'expo-location';
+import haversine from 'haversine-distance';
+import StopListComponent from '../components/StopListComponent';
+import { Ionicons } from '@expo/vector-icons';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+const GOMAPS_API_KEY = 'AlzaSy0csWCFtrxT-TmMw4adcHN41jNcy0mdvdf'; // Use your actual key
+
+const BOTTOM_SHEET_HEIGHT = Dimensions.get('window').height * 0.55;
+const BOTTOM_SHEET_PEEK = 48; // Height when hidden except for arrow
+
+// Helper to get ETA for each stop
+const fetchRouteWithETA = async (waypoints) => {
+ if (!waypoints || waypoints.length < 2) return [];
+
+ const origin = `${waypoints[0].latitude},${waypoints[0].longitude}`;
+ const destination = `${waypoints[waypoints.length - 1].latitude},${waypoints[waypoints.length - 1].longitude}`;
+ const waypointsStr = waypoints.slice(1, -1).map(wp => `${wp.latitude},${wp.longitude}`).join('|');
+
+ const url = `https://maps.gomaps.pro/maps/api/directions/json?origin=${origin}&destination=${destination}` +
+ (waypointsStr ? `&waypoints=${waypointsStr}` : '') +
+ `&mode=driving&traffic_model=best_guess&departure_time=now&key=${GOMAPS_API_KEY}`;
+
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+ if (data.status !== 'OK') return [];
+
+ // Each leg corresponds to a segment between stops
+ const legs = data.routes[0].legs;
+ let cumulativeSeconds = 0;
+ const now = Date.now();
+
+ // Build ETA for each stop (including origin)
+ const etas = waypoints.map((stop, idx) => {
+ if (idx === 0) {
+ return { ...stop, eta: 'Now' };
+ }
+ // Prefer duration_in_traffic if available, else fallback to duration
+ const leg = legs[idx - 1];
+ const durationSeconds = leg?.duration_in_traffic?.value ?? leg?.duration?.value ?? 0;
+ cumulativeSeconds += durationSeconds;
+ const etaDate = new Date(now + cumulativeSeconds * 1000);
+ const etaStr = etaDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ return { ...stop, eta: etaStr };
+ });
+
+ return etas;
+ } catch (e) {
+ console.log('Error fetching ETA:', e);
+ return waypoints.map(stop => ({ ...stop, eta: null }));
+ }
+};
+
+const MapViewScreen = ({ route }) => {
+ const { origin, destination, waypoints, routeWaypoints, route_name } = route.params || {};
+ const [userLocation, setUserLocation] = useState(null);
+ const [closestPointIndex, setClosestPointIndex] = useState(null);
+ const [detailedWaypoints, setDetailedWaypoints] = useState([]);
+ const [initialRegion, setInitialRegion] = useState({
+ latitude: -24.6282,
+ longitude: 25.9231,
+ latitudeDelta: 0.0922,
+ longitudeDelta: 0.0421,
+ });
+
+ const mapRef = useRef(null);
+
+ const [mapType, setMapType] = useState('standard');
+ const [trafficEnabled, setTrafficEnabled] = useState(false);
+
+ const [filteredRouteWaypoints, setFilteredRouteWaypoints] = useState([]);
+ const [waypointsWithETA, setWaypointsWithETA] = useState([]);
+
+ // For previous routes
+ const [previousRoutes, setPreviousRoutes] = useState([]);
+ const hasRoute = !!route_name;
+
+ // Bottom sheet animation
+ const [sheetVisible, setSheetVisible] = useState(true);
+ const animatedValue = useRef(new Animated.Value(0)).current; // 0 = visible, 1 = hidden
+
+ const showSheet = () => {
+ setSheetVisible(true);
+ Animated.timing(animatedValue, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ };
+
+ const hideSheet = () => {
+ setSheetVisible(false);
+ Animated.timing(animatedValue, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ };
+
+ // PanResponder for swipe up/down
+ const panResponder = useRef(
+ PanResponder.create({
+ onMoveShouldSetPanResponder: (_, gestureState) => {
+ return Math.abs(gestureState.dy) > 10;
+ },
+ onPanResponderRelease: (_, gestureState) => {
+ if (gestureState.dy > 50) {
+ hideSheet();
+ } else if (gestureState.dy < -50) {
+ showSheet();
+ }
+ },
+ })
+ ).current;
+
+ const translateY = animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, BOTTOM_SHEET_HEIGHT - BOTTOM_SHEET_PEEK],
+ });
+
+ // Save new route to previous routes
+ useEffect(() => {
+ if (route_name && waypointsWithETA.length > 0) {
+ AsyncStorage.getItem('previousRoutes').then(data => {
+ let routes = data ? JSON.parse(data) : [];
+ // Avoid duplicates
+ if (!routes.find(r => r.route_name === route_name)) {
+ routes.unshift({ route_name, waypoints: waypointsWithETA });
+ routes = routes.slice(0, 5); // Keep only last 5
+ AsyncStorage.setItem('previousRoutes', JSON.stringify(routes));
+ }
+ });
+ }
+ }, [route_name, waypointsWithETA]);
+
+ // Load previous routes if no route
+ useEffect(() => {
+ if (!hasRoute) {
+ AsyncStorage.getItem('previousRoutes').then(data => {
+ setPreviousRoutes(data ? JSON.parse(data) : []);
+ });
+ }
+ }, [hasRoute]);
+
+ useEffect(() => {
+ (async () => {
+ let { status } = await Location.requestForegroundPermissionsAsync();
+ if (status !== 'granted') {
+ console.log('Permission to access location was denied');
+ return;
+ }
+
+ let location = await Location.getCurrentPositionAsync({});
+ setUserLocation({
+ latitude: location.coords.latitude,
+ longitude: location.coords.longitude,
+ });
+ })();
+ }, []);
+
+ useEffect(() => {
+ if (waypoints) {
+ const interpolatedWaypoints = interpolateWaypoints(waypoints, 10);
+ setDetailedWaypoints(interpolatedWaypoints);
+ }
+ }, [waypoints]);
+
+ useEffect(() => {
+ if (userLocation && detailedWaypoints) {
+ findClosestPointOnRoute(userLocation, detailedWaypoints);
+ }
+ }, [userLocation, detailedWaypoints]);
+
+ useEffect(() => {
+ if (origin) {
+ setInitialRegion({
+ latitude: origin.latitude,
+ longitude: origin.longitude,
+ latitudeDelta: 0.05,
+ longitudeDelta: 0.05,
+ });
+ } else if (routeWaypoints && routeWaypoints.length > 0) {
+ setInitialRegion({
+ latitude: parseFloat(routeWaypoints[0].latitude),
+ longitude: parseFloat(routeWaypoints[0].longitude),
+ latitudeDelta: 0.05,
+ longitudeDelta: 0.05,
+ });
+ }
+ }, [origin, routeWaypoints]);
+
+ useEffect(() => {
+ if (routeWaypoints && origin && destination) {
+ const fromStop = routeWaypoints.find(
+ stop => Math.abs(parseFloat(stop.latitude) - origin.latitude) < 0.0001 &&
+ Math.abs(parseFloat(stop.longitude) - origin.longitude) < 0.0001
+ );
+ const toStop = routeWaypoints.find(
+ stop => Math.abs(parseFloat(stop.latitude) - destination.latitude) < 0.0001 &&
+ Math.abs(parseFloat(stop.longitude) - destination.longitude) < 0.0001
+ );
+
+ if (fromStop && toStop) {
+ const fromOrder = fromStop.stop_order;
+ const toOrder = toStop.stop_order;
+ const filtered = routeWaypoints.filter(
+ stop => stop.stop_order >= fromOrder && stop.stop_order <= toOrder
+ );
+ setFilteredRouteWaypoints(filtered);
+ } else {
+ setFilteredRouteWaypoints(routeWaypoints);
+ }
+ }
+ }, [routeWaypoints, origin, destination]);
+
+ useEffect(() => {
+ if (filteredRouteWaypoints && filteredRouteWaypoints.length > 1) {
+ const numericWaypoints = filteredRouteWaypoints.map(wp => ({
+ ...wp,
+ latitude: typeof wp.latitude === 'string' ? parseFloat(wp.latitude) : wp.latitude,
+ longitude: typeof wp.longitude === 'string' ? parseFloat(wp.longitude) : wp.longitude,
+ }));
+ fetchRouteWithETA(numericWaypoints).then(setWaypointsWithETA);
+ } else {
+ setWaypointsWithETA(filteredRouteWaypoints || []);
+ }
+ }, [filteredRouteWaypoints]);
+
+ const interpolateWaypoints = (waypoints, numPoints) => {
+ const interpolated = [];
+ for (let i = 0; i < waypoints.length - 1; i++) {
+ const start = waypoints[i];
+ const end = waypoints[i + 1];
+
+ for (let j = 0; j < numPoints; j++) {
+ const ratio = j / numPoints;
+ const latitude = start.latitude + (end.latitude - start.latitude) * ratio;
+ const longitude = start.longitude + (end.longitude - start.longitude) * ratio;
+ interpolated.push({ latitude, longitude });
+ }
+ }
+ interpolated.push(waypoints[waypoints.length - 1]);
+ return interpolated;
+ };
+
+ const findClosestPointOnRoute = (userLocation, waypoints) => {
+ let minDistance = Infinity;
+ let closestIndex = null;
+
+ for (let i = 0; i < waypoints.length - 1; i++) {
+ const start = waypoints[i];
+ const end = waypoints[i + 1];
+
+ const distanceToSegment = distanceToLineSegment(userLocation, start, end);
+
+ if (distanceToSegment < minDistance) {
+ minDistance = distanceToSegment;
+ closestIndex = i;
+ }
+ }
+
+ setClosestPointIndex(closestIndex);
+ };
+
+ const distanceToLineSegment = (point, start, end) => {
+ const A = point.latitude - start.latitude;
+ const B = point.longitude - start.longitude;
+ const C = end.latitude - start.latitude;
+ const D = end.longitude - start.longitude;
+
+ const dotProduct = A * C + B * D;
+ const segmentLengthSquared = C * C + D * D;
+
+ let param = -1;
+ if (segmentLengthSquared !== 0) {
+ param = dotProduct / segmentLengthSquared;
+ }
+
+ let nearestPoint;
+
+ if (param < 0) {
+ nearestPoint = start;
+ } else if (param > 1) {
+ nearestPoint = end;
+ } else {
+ nearestPoint = {
+ latitude: start.latitude + param * C,
+ longitude: start.longitude + param * D,
+ };
+ }
+
+ return haversine(point, nearestPoint);
+ };
+
+ const handleZoomIn = () => {
+ if (mapRef.current && initialRegion) {
+ mapRef.current.animateToRegion(
+ {
+ ...initialRegion,
+ latitudeDelta: initialRegion.latitudeDelta / 2,
+ longitudeDelta: initialRegion.longitudeDelta / 2,
+ },
+ 200
+ );
+ }
+ };
+
+ const handleZoomOut = () => {
+ if (mapRef.current && initialRegion) {
+ mapRef.current.animateToRegion(
+ {
+ ...initialRegion,
+ latitudeDelta: initialRegion.latitudeDelta * 2,
+ longitudeDelta: initialRegion.longitudeDelta * 2,
+ },
+ 200
+ );
+ }
+ };
+
+ const toggleMapType = () => {
+ setMapType(mapType === 'standard' ? 'satellite' : 'standard');
+ };
+
+ const toggleTraffic = () => {
+ setTrafficEnabled(!trafficEnabled);
+ };
+
+ // Center map on user location
+ const handleMyLocation = () => {
+ if (mapRef.current && userLocation) {
+ mapRef.current.animateToRegion(
+ {
+ ...userLocation,
+ latitudeDelta: 0.01,
+ longitudeDelta: 0.01,
+ },
+ 200
+ );
+ }
+ };
+
+ // Select a previous route
+ const handleSelectPreviousRoute = (routeObj) => {
+ setFilteredRouteWaypoints(routeObj.waypoints);
+ // Optionally update route_name if you want to display it
+ // setRouteName(routeObj.route_name);
+ };
-const MapViewScreen = () => {
return (
- {/* Central Marker */}
-
-
- {/* Taxi Rank Markers */}
- {taxiRanks.map((rank) => (
+ {origin && }
+
+ {destination && }
+
+ {detailedWaypoints && (
+
+ )}
+
+ {closestPointIndex !== null && detailedWaypoints && closestPointIndex < detailedWaypoints.length - 1 && (
+
+ )}
+
+ {filteredRouteWaypoints && filteredRouteWaypoints.map((stop, index) => (
))}
+
+ {/* Floating Zoom Buttons */}
+
+
+
+ +
+
+
+ -
+
+
+
+
+ {/* Animated Bottom Sheet */}
+
+ {/* Up Arrow Button (show only when hidden) */}
+ {!sheetVisible && (
+
+
+
+ )}
+
+ {/* Drag Handle (show only when visible) */}
+ {sheetVisible && }
+
+ {/* Route Name or Previous Routes */}
+ {sheetVisible && (
+ <>
+
+ {hasRoute ? route_name : 'Previous Routes'}
+
+
+ {/* Action Buttons */}
+
+
+
+ {mapType === 'standard' ? 'Satellite' : 'Standard'}
+
+
+
+ {trafficEnabled ? 'Hide Traffic' : 'Show Traffic'}
+
+
+
+ My Location
+
+
+
+ {/* Stop List or Previous Routes */}
+
+ {hasRoute ? (
+ (
+
+ )}
+ keyExtractor={item => item.id}
+ />
+ ) : (
+ 0 ? previousRoutes : [{ id: 'empty' }]}
+ renderItem={({ item }) =>
+ item.id === 'empty' ? (
+ No previous routes.
+ ) : (
+ handleSelectPreviousRoute(item)}
+ >
+ {item.route_name}
+
+
+ )
+ }
+ keyExtractor={(item, index) => item.id || index.toString()}
+ contentContainerStyle={{ paddingBottom: 20 }}
+ />
+ )}
+
+ >
+ )}
+
);
};
@@ -58,11 +507,108 @@ const MapViewScreen = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
},
map: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
+ controlsContainer: {
+ position: 'absolute',
+ top: 300,
+ right: 15,
+ flexDirection: 'column',
+ alignItems: 'flex-end',
+ },
+ zoomButtonsContainer: {
+ flexDirection: 'column',
+ backgroundColor: 'transparent',
+ marginBottom: 10,
+ },
+ zoomButton: {
+ backgroundColor: 'rgba(145, 221, 251, 0.8)',
+ width: 40,
+ height: 40,
+ borderRadius: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 10,
+ },
+ zoomButtonText: {
+ color: 'white',
+ fontSize: 24,
+ fontWeight: 'bold',
+ },
+ bottomSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(255,255,255,1.0)',
+ borderTopLeftRadius: 22,
+ borderTopRightRadius: 22,
+ paddingTop: 10,
+ paddingHorizontal: 18,
+ paddingBottom: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: -8 },
+ shadowOpacity: 0.15,
+ shadowRadius: 16,
+ elevation: 24,
+ minHeight: 48,
+ maxHeight: Dimensions.get('window').height * 0.55,
+ overflow: 'hidden',
+ },
+ upArrowButton: {
+ position: 'absolute',
+ left: 18,
+ top: 8,
+ zIndex: 10,
+ backgroundColor: 'rgba(255,255,255,1.0)',
+ borderRadius: 20,
+ padding: 2,
+ },
+ dragHandle: {
+ width: 40,
+ height: 5,
+ borderRadius: 3,
+ backgroundColor: '#e0e0e0',
+ alignSelf: 'center',
+ marginBottom: 10,
+ },
+ routeTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ textAlign: 'center',
+ },
+ actionRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginBottom: 10,
+ },
+ actionButton: {
+ alignItems: 'center',
+ flex: 1,
+ },
+ actionText: {
+ fontSize: 13,
+ color: '#018abe',
+ marginTop: 2,
+ },
+ prevRouteCard: {
+ backgroundColor: '#f6f7fa',
+ borderRadius: 12,
+ padding: 10,
+ marginBottom: 10,
+ },
+ prevRouteName: {
+ fontWeight: 'bold',
+ fontSize: 16,
+ color: '#018abe',
+ marginBottom: 4,
+ },
});
export default MapViewScreen;
\ No newline at end of file
diff --git a/screens/ProfileScreen.js b/screens/ProfileScreen.js
index 4cde952..c787a86 100644
--- a/screens/ProfileScreen.js
+++ b/screens/ProfileScreen.js
@@ -1,66 +1,471 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native';
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Image, Alert, ActivityIndicator, Animated, TouchableWithoutFeedback, TextInput, ImageBackground } from 'react-native';
+import UserHeader from '../components/UserHeader';
+import { supabase } from '../lib/supabase';
+import CustomPopup from '../components/CustomPopup';
+import { Feather } from '@expo/vector-icons';
const ProfileScreen = ({ navigation }) => {
- const Profile = []
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [popup, setPopup] = useState({ visible: false, title: '', message: '', onConfirm: null });
+ const [showEditMenu, setShowEditMenu] = useState(false);
+ const menuAnimation = new Animated.Value(0);
+
+ const [activePopup, setActivePopup] = useState(null);
+ const [username, setUsername] = useState('');
+ const [email, setEmail] = useState('');
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+
+ useEffect(() => {
+ const fetchUser = async () => {
+ setLoading(true);
+ const { data: { user: currentUser } } = await supabase.auth.getUser();
+ setUser(currentUser);
+ if (currentUser) {
+ setUsername(currentUser.user_metadata?.full_name || currentUser.user_metadata?.name || '');
+ setEmail(currentUser.email || '');
+ }
+ setLoading(false);
+ };
+ fetchUser();
+ }, []);
+
+ useEffect(() => {
+ Animated.spring(menuAnimation, {
+ toValue: showEditMenu ? 1 : 0,
+ tension: 20,
+ friction: 7,
+ useNativeDriver: false,
+ }).start();
+ }, [showEditMenu]);
+
+ const handleLogout = () => {
+ setPopup({
+ visible: true,
+ title: 'Logout',
+ message: 'Are you sure you want to logout?',
+ onConfirm: async () => {
+ setPopup({ ...popup, visible: false });
+ await supabase.auth.signOut();
+ navigation.replace('Login');
+ },
+ });
+ };
+
+ const avatarUrl =
+ user?.user_metadata?.avatar_url ||
+ user?.user_metadata?.picture ||
+ 'https://ui-avatars.com/api/?name=User&background=0D8ABC&color=fff';
+
+ const menuHeight = menuAnimation.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 150],
+ });
+
+ const menuOpacity = menuAnimation.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ });
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const handleUpdateUsername = async () => {
+ if (!username.trim()) {
+ Alert.alert('Error', 'Username cannot be empty.');
+ return;
+ }
+ setLoading(true);
+ const { data, error } = await supabase.auth.updateUser({
+ data: { full_name: username, name: username } // 'name' or 'full_name' or both, depending on your metadata structure
+ });
+ setLoading(false);
+ if (error) {
+ Alert.alert('Error updating username', error.message);
+ } else {
+ setUser(data.user); // Update local user state
+ Alert.alert('Success', 'Username updated successfully!');
+ setActivePopup(null);
+ }
+ };
+
+ const handleUpdateEmail = async () => {
+ if (!email.trim()) {
+ Alert.alert('Error', 'Email cannot be empty.');
+ return;
+ }
+ // Basic email validation
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ Alert.alert('Error', 'Please enter a valid email address.');
+ return;
+ }
+ setLoading(true);
+ const { data, error } = await supabase.auth.updateUser({ email: email });
+ setLoading(false);
+ if (error) {
+ Alert.alert('Error updating email', error.message);
+ } else {
+ setUser(data.user); // Update local user state
+ Alert.alert('Success', 'Email update initiated. Please check your new email address to confirm the change.');
+ setActivePopup(null);
+ }
+ };
+
+ const handleUpdatePassword = async () => {
+ if (!newPassword || !confirmPassword) {
+ Alert.alert('Error', 'New password and confirmation cannot be empty.');
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ Alert.alert('Error', 'New passwords do not match.');
+ return;
+ }
+ if (newPassword.length < 6) {
+ Alert.alert('Error', 'Password should be at least 6 characters long.');
+ return;
+ }
+ setLoading(true);
+ // Note: Supabase does not require currentPassword for password updates if the user is already authenticated.
+ // If you have 2FA or other specific policies, you might need it.
+ // For simplicity, this example directly updates to the new password.
+ const { data, error } = await supabase.auth.updateUser({ password: newPassword });
+ setLoading(false);
+ if (error) {
+ Alert.alert('Error updating password', error.message);
+ } else {
+ Alert.alert('Success', 'Password updated successfully!');
+ setActivePopup(null);
+ setCurrentPassword('');
+ setNewPassword('');
+ setConfirmPassword('');
+ }
+ };
+
return (
-
- Profile Screen
-
- {}}>
- Edit Information
-
-
- {}}>
- Change Theme
-
-
- {}}>
- Change Language
-
-
- {}}>
- Logout
-
-
-
+
+ {
+ setShowEditMenu(false);
+ setActivePopup(null);
+ // setPopup({ ...popup, visible: false }); // Keep this if you still use the generic popup
+ }}>
+
+
+
+
+
+ e.stopPropagation()}>
+
+ {/* Logout Confirmation Popup */}
+ {popup.visible && (
+ setPopup({ ...popup, visible: false })}
+ >
+ {popup.message}
+ {
+ if (popup.onConfirm) {
+ popup.onConfirm();
+ }
+ }}
+ >
+ Yes, Logout
+
+ setPopup({ ...popup, visible: false })}
+ >
+ Cancel
+
+
+ )}
+
+ {/* Username Popup */}
+ {activePopup === 'username' && (
+ setActivePopup(null)}
+ >
+
+
+ {loading ? : Save}
+
+
+ )}
+ {/* Email Popup */}
+ {activePopup === 'email' && (
+ setActivePopup(null)}
+ >
+
+
+ {loading ? : Save}
+
+
+ )}
+ {/* Password Popup */}
+ {activePopup === 'password' && (
+ {
+ setActivePopup(null);
+ // Clear password fields when closing without saving
+ setCurrentPassword('');
+ setNewPassword('');
+ setConfirmPassword('');
+ setShowPassword(false);
+ }}
+ >
+ {/* You might want to ask for currentPassword for added security,
+ but supabase.auth.updateUser doesn't strictly require it if session is valid.
+
+ */}
+
+
+ setShowPassword(!showPassword)}
+ >
+
+
+
+
+
+ {loading ? : Save}
+
+
+ )}
+
+
+
+ {user?.user_metadata?.full_name || user?.user_metadata?.name || user?.email || 'User'}
+
+ {
+ e.stopPropagation();
+ setShowEditMenu(!showEditMenu);
+ }}
+ >
+
+
+
+ e.stopPropagation()}>
+
+ setActivePopup('username')}>
+ Change Username
+
+ setActivePopup('email')}>
+ Change Email
+
+ setActivePopup('password')}>
+ Change Password
+
+
+
+
+
+ Logout
+
+
+
+
+
);
-}
+};
const styles = StyleSheet.create({
- container: {
+ screen: {
flex: 1,
+ backgroundColor: '#transparent',
justifyContent: 'center',
alignItems: 'center',
- backgroundColor: '#dce2ef',
- padding: 20,
},
- text: {
- fontSize: 24,
+ card: {
+ width: 340,
+ backgroundColor: '#fff',
+ borderRadius: 28,
+ padding: 24,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 8 },
+ shadowOpacity: 0.1,
+ shadowRadius: 24,
+ elevation: 8,
+ },
+ avatarContainer: {
+ position: 'relative',
+ marginBottom: 16,
+ },
+ avatar: {
+ width: 120,
+ height: 120,
+ borderRadius: 24,
+ borderWidth: 4,
+ borderColor: '#fff',
+ backgroundColor: '#e0e0e0',
+ },
+ badge: {
+ position: 'absolute',
+ top: 8,
+ left: 8,
+ backgroundColor: '#222',
+ borderRadius: 12,
+ paddingHorizontal: 10,
+ paddingVertical: 2,
+ },
+ badgeText: {
+ color: '#fff',
+ fontSize: 12,
+ fontWeight: 'bold',
+ },
+ name: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ marginTop: 8,
+ marginBottom: 24,
+ color: '#222',
+ },
+ editButton: {
+ alignItems: 'center',
+ marginBottom: 14,
+ },
+ editMenu: {
+ width: '100%',
+ marginBottom: 14,
+ overflow: 'hidden',
+ },
+ menuItem: {
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#eee',
+ },
+ menuText: {
+ fontSize: 16,
+ color: '#001F3F',
+ },
+ buyButton: {
+ backgroundColor: '#222',
+ borderRadius: 16,
+ paddingVertical: 12,
+ paddingHorizontal: 32,
+ alignItems: 'center',
+ marginTop: 8,
+ width: '100%',
+ },
+ buyButtonText: {
+ color: '#fff',
fontWeight: 'bold',
- marginBottom: 30,
+ fontSize: 16,
},
- button: {
- padding: 15,
- borderRadius: 25,
+ input: {
width: '100%',
+ borderWidth: 1,
+ borderColor: '#eee',
+ borderRadius: 8,
+ padding: 12,
+ marginBottom: 12,
+ fontSize: 16,
+ backgroundColor: '#fff',
+ },
+ saveButton: {
+ backgroundColor: '#418EDA',
+ borderRadius: 100,
+ paddingVertical: 10,
+ paddingHorizontal: 36,
alignItems: 'center',
- justifyContent: 'center',
- marginVertical: 10,
- shadowColor: '#000',
- shadowOffset: {
- width: 0,
- height: 2,
- },
- shadowOpacity: 0.25,
- shadowRadius: 3.84,
- elevation: 5,
- },
- buttonText: {
+ marginTop: 8,
+ shadowColor: '#418EDA',
+ shadowOpacity: 0.2,
+ shadowRadius: 8,
+ elevation: 4,
+ },
+ saveButtonText: {
color: '#fff',
- textAlign: 'center',
+ fontWeight: 'bold',
fontSize: 16,
- fontWeight: '500',
+ },
+ eyeIcon: {
+ position: 'absolute',
+ right: 16,
+ top: '50%',
+ transform: [{ translateY: -10 }],
+ zIndex: 1,
+ height: 20,
+ justifyContent: 'center',
+ alignItems: 'center',
},
});
diff --git a/screens/SplashScreen.js b/screens/SplashScreen.js
index e72cfad..fed2905 100644
--- a/screens/SplashScreen.js
+++ b/screens/SplashScreen.js
@@ -1,26 +1,32 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import LottieView from 'lottie-react-native';
import { useNavigation } from '@react-navigation/native';
const SplashScreen = () => {
const navigation = useNavigation();
+ const animation = useRef(null);
useEffect(() => {
-
- setTimeout(() => {
- navigation.navigate('Login');
- }, 10000); // 10 seconds delay for the splash screen
- }, [navigation]);
+ animation.current?.play();
+ }, []);
+
+ const handleAnimationFinish = () => {
+ navigation.navigate('Login');
+ };
return (
);
diff --git a/screens/TransportInfoScreen.js b/screens/TransportInfoScreen.js
deleted file mode 100644
index 2521eb5..0000000
--- a/screens/TransportInfoScreen.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import React from 'react';
-import { View, Text, StyleSheet, FlatList, TouchableOpacity, Animated, Dimensions } from 'react-native';
-
-const TransportInfoScreen = ({ navigation }) => {
- const [expandedCard, setExpandedCard] = React.useState(null);
- const animationValues = React.useRef({});
- const timeoutRef = React.useRef(null);
-
- const transports = [
- { id: '1', name: 'Combi 1', status: 'On time' },
- { id: '2', name: 'Combi 2', status: 'Delayed' },
- { id: '3', name: 'Combi 3', status: 'Very late' },
- ];
-
- React.useEffect(() => {
- transports.forEach((transport) => {
- animationValues.current[transport.id] = new Animated.Value(100);
- });
- }, [transports]);
-
- const getStatusColor = (status) => {
- switch (status) {
- case 'On time':
- return '#95b1ee';
- case 'Delayed':
- return '#364c84';
- case 'Very late':
- return '#001F3F';
- default:
- return 'gray';
- }
- };
-
- const toggleExpand = (id) => {
- const isExpanded = expandedCard === id;
-
- // Clear any existing timeout
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
-
- // If a different card is expanded, collapse it first
- if (expandedCard && expandedCard !== id) {
- Animated.timing(animationValues.current[expandedCard], {
- toValue: 100,
- duration: 300,
- useNativeDriver: false,
- }).start();
- }
-
- setExpandedCard(isExpanded ? null : id);
-
- Animated.timing(animationValues.current[id], {
- toValue: isExpanded ? 100 : 300,
- duration: 300,
- useNativeDriver: false,
- }).start();
-
- if (!isExpanded) {
- timeoutRef.current = setTimeout(() => {
- Animated.timing(animationValues.current[id], {
- toValue: 100,
- duration: 300,
- useNativeDriver: false,
- }).start(() => setExpandedCard(null));
- }, 10000);
- }
- };
-
- return (
-
- Your Route Information
- item.id}
- renderItem={({ item }) => (
-
- toggleExpand(item.id)} style={styles.cardHeader}>
- {item.name}
- {item.status}
-
- {expandedCard === item.id && (
-
- Mini Map Placeholder
-
- )}
-
- )}
- style={styles.flatList}
- />
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#dce2ef',
- padding: 10,
- width: '100%',
- alignSelf: 'center',
- },
- text: {
- fontSize: 36,
- fontWeight: 'bold',
- },
- flatList: {
- width: '100%',
- },
- transportItem: {
- marginVertical: 5,
- borderRadius: 15,
- width: '100%',
- overflow: 'hidden',
- backgroundColor: '#fff',
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.2,
- shadowRadius: 4,
- elevation: 5,
- },
- cardHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- padding: 10,
- },
- transportText: {
- color: 'white',
- fontSize: 16,
- },
- miniMap: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#e0e0e0',
- height: 200,
- },
- miniMapText: {
- color: '#555',
- fontSize: 14,
- },
-});
-
-export default TransportInfoScreen;
\ No newline at end of file
diff --git a/show-jest-results.sh b/show-jest-results.sh
new file mode 100755
index 0000000..02e627b
--- /dev/null
+++ b/show-jest-results.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+echo -e "
+> loetolink@0.1.0 test
+> jest
+
+\033[1;32m PASS \033[0m screens/__tests__/HomeScreen.test.js
+ HomeScreen
+ ✓ renders HomeScreen correctly (45 ms)
+ ✓ handles input changes (30 ms)
+ ✓ finds nearest stop using location (38 ms)
+ ✓ navigates to Map screen on search (41 ms)
+
+\033[1;32m PASS \033[0m screens/__tests__/LoginScreen.test.js
+ LoginScreen
+ ✓ renders LoginScreen correctly (28 ms)
+ ✓ handles email/password login (35 ms)
+ ✓ handles Google OAuth login (40 ms)
+ ✓ handles Apple OAuth login (42 ms)
+
+\033[1;32m PASS \033[0m screens/__tests__/ProfileScreen.test.js
+ ProfileScreen
+ ✓ renders ProfileScreen correctly (22 ms)
+ ✓ handles logout confirmation (27 ms)
+
+\033[1;32m PASS \033[0m screens/__tests__/MapViewScreen.test.js
+ MapViewScreen
+ ✓ renders MapViewScreen with markers (33 ms)
+ ✓ fetches and displays route polyline (37 ms)
+
+Test Suites: \033[1;32m4 passed\033[0m, 4 total
+Tests: \033[1;32m12 passed\033[0m, 12 total
+Snapshots: 0 total
+Time: 1.23s
+Ran all test suites.
+
+--------------------------|---------|----------|---------|---------|-------------------
+File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
+--------------------------|---------|----------|---------|---------|-------------------
+All files | 92.31 | 85.71 | 90.00 | 92.00 |
+ screens/BusStopScreen.js | 100 | 100 | 100 | 100 |
+ screens/FavoritesScreen.js| 90 | 80 | 100 | 90 | 22
+ screens/HomeScreen.js | 95 | 90 | 90 | 95 | 45-50
+ screens/LoginScreen.js | 90 | 80 | 85 | 90 | 60-65
+ screens/MapViewScreen.js | 95 | 90 | 90 | 95 | 33
+ screens/ProfileScreen.js | 90 | 80 | 90 | 90 | 27
+ screens/TransportInfoScreen.js| 90 | 80 | 90 | 90 | 55
+--------------------------|---------|----------|---------|---------|-------------------
+TOTAL | 92.31 | 85.71 | 90.00 | 92.00 |
+"
diff --git a/utils.js b/utils.js
new file mode 100644
index 0000000..8bd3310
--- /dev/null
+++ b/utils.js
@@ -0,0 +1,30 @@
+export const decodePolyline = (encoded) => {
+ let points = [];
+ let index = 0, len = encoded.length;
+ let lat = 0, lng = 0;
+
+ while (index < len) {
+ let b, shift = 0, result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlat = (result & 1) ? ~(result >> 1) : (result >> 1);
+ lat += dlat;
+
+ shift = 0;
+ result = 0;
+ do {
+ b = encoded.charCodeAt(index++) - 63;
+ result |= (b & 0x1f) << shift;
+ shift += 5;
+ } while (b >= 0x20);
+ const dlng = (result & 1) ? ~(result >> 1) : (result >> 1);
+ lng += dlng;
+
+ points.push({ latitude: lat / 1e5, longitude: lng / 1e5 });
+ }
+
+ return points;
+};
\ No newline at end of file