diff --git a/App.js b/App.js
index 7e2af9c..f1f9277 100644
--- a/App.js
+++ b/App.js
@@ -1,15 +1,13 @@
import React from 'react';
import { StatusBar, SafeAreaView } from 'react-native';
import AppNavigator from './navigation/AppNavigator';
-import Auth from './components/Auth';
+
export default function App() {
return (
<>
-
-
-
+
>
);
diff --git a/app.json b/app.json
index e186769..88481e7 100644
--- a/app.json
+++ b/app.json
@@ -21,7 +21,7 @@
"android": {
"config": {
"googleMaps": {
- "apiKey": "AlzaSya0Ll6laKMO06oO9lBmTdqQ6P_4Qo1_fEJ"
+ "apiKey": "AlzaSykD0-TOgCvku5D5nyYC67DmWk2aaon-COna"
}
},
"adaptiveIcon": {
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/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/UserHeader.js b/components/UserHeader.js
new file mode 100644
index 0000000..ad4ecbd
--- /dev/null
+++ b/components/UserHeader.js
@@ -0,0 +1,93 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, Image, StyleSheet } from 'react-native';
+import { supabase } from '../lib/supabase';
+
+const fallbackAvatar = require('../assets/avatar.jpg');
+
+const UserHeader = ({ minimal = false }) => {
+ 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();
+ }, []);
+
+ if (minimal) {
+ // Render only the profile picture
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ Hey, {user?.name || 'User'}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ margin: 10,
+ marginTop: 40,
+ padding: 10,
+ borderRadius: 50,
+ shadowColor: '#000',
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 2,
+ justifyContent: 'space-between',
+ },
+ avatar: {
+ width: 40,
+ height: 40,
+ borderRadius: 50,
+ marginRight: 16,
+ },
+ avatarLarge: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ },
+ 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/navigation/AppNavigator.js b/navigation/AppNavigator.js
index 84c194a..198afb6 100644
--- a/navigation/AppNavigator.js
+++ b/navigation/AppNavigator.js
@@ -6,6 +6,7 @@ import { Ionicons } from '@expo/vector-icons';
import { Animated } from 'react-native';
import { CardStyleInterpolators } from '@react-navigation/stack';
+
// Auth Screens
import SplashScreen from '../screens/SplashScreen';
import LoginScreen from '../screens/LoginScreen';
@@ -15,7 +16,7 @@ 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 MostUsedScreen from '../screens/MostUsedScreen';
import ProfileScreen from '../screens/ProfileScreen';
const Tab = createBottomTabNavigator();
@@ -28,7 +29,7 @@ const MainTabNavigator = () => {
return (
({
- headerShown: false,
+ headerShown: false, // Ensure no header is shown
tabBarIcon: ({ focused, color, size }) => {
let iconName;
@@ -40,8 +41,8 @@ const MainTabNavigator = () => {
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 === 'Most Used') {
+ iconName = focused ? 'star' : 'star-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
}
@@ -50,16 +51,13 @@ const MainTabNavigator = () => {
},
tabBarActiveTintColor: '#acd4fd',
tabBarInactiveTintColor: '#fff',
- headerShown: false,
tabBarShowLabel: true,
-
tabBarStyle: {
position: 'absolute',
height: 60,
paddingBottom: 9,
paddingTop: 8,
backgroundColor: 'black',
-
left: '15%',
right: '15%',
bottom: 20,
@@ -72,14 +70,14 @@ const MainTabNavigator = () => {
margin: 20,
},
tabBarHideOnKeyboard: false,
- animationEnabled: false
+ animationEnabled: false,
})}
>
-
+
);
@@ -88,12 +86,12 @@ const MainTabNavigator = () => {
// Auth stack navigator
const AuthStack = () => {
return (
-
@@ -122,12 +120,12 @@ const RootNavigator = () => {
}
return (
-
{isAuthenticated ? (
@@ -136,6 +134,7 @@ const RootNavigator = () => {
<>
+
>
)}
diff --git a/package-lock.json b/package-lock.json
index c52d009..6c5b265 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,12 @@
"@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",
+ "haversine-distance": "^1.2.3",
"lottie-react-native": "^7.1.0",
"react": "18.3.1",
"react-native": "0.76.9",
@@ -7359,6 +7364,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 +7400,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 +7432,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 +7480,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 +7577,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",
@@ -8052,6 +8139,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",
diff --git a/package.json b/package.json
index 56a002c..b105459 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,12 @@
"@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",
+ "haversine-distance": "^1.2.3",
"lottie-react-native": "^7.1.0",
"react": "18.3.1",
"react-native": "0.76.9",
diff --git a/screens/BusStopScreen.js b/screens/BusStopScreen.js
index ba9de66..aeed62f 100644
--- a/screens/BusStopScreen.js
+++ b/screens/BusStopScreen.js
@@ -1,12 +1,53 @@
-import React from 'react';
-import { View, Text, StyleSheet } from 'react-native';
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, TextInput, FlatList, TouchableOpacity } from 'react-native';
import Button from '../components/Button';
const BusStopScreen = ({ navigation }) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [filteredStops, setFilteredStops] = useState([]);
+
+ // Example bus stops data - replace with your actual data
+ const busStops = [
+ { id: '1', name: 'Gamecity bus stop' },
+ { id: '2', name: 'Apex bus stop' },
+ { id: '3', name: 'BAC bus stop' },
+ // Add more bus stops as needed
+ ];
+
+ const handleSearch = (text) => {
+ setSearchQuery(text);
+ const filtered = busStops.filter(stop =>
+ stop.name.toLowerCase().includes(text.toLowerCase())
+ );
+ setFilteredStops(filtered);
+ };
+
return (
- Bus Stop Screen
-
);
};
@@ -14,14 +55,50 @@ const BusStopScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
+
backgroundColor: '#dce2ef',
padding: 20,
},
+ searchContainer: {
+ width: '100%',
+ marginBottom: 20,
+ },
+ searchBar: {
+ backgroundColor: 'white',
+ padding: 10,
+ borderRadius: 10,
+ marginBottom: 5,
+ },
+ dropdown: {
+ backgroundColor: 'white',
+ maxHeight: 200,
+ borderRadius: 10,
+ },
+ dropdownItemContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: 15,
+ borderBottomWidth: 1,
+ borderBottomColor: '#eee',
+ },
+ dropdownItem: {
+ flex: 1,
+ },
+ routeButton: {
+ backgroundColor: '#22303f',
+ padding: 8,
+ borderRadius: 5,
+ marginLeft: 10,
+ },
+ routeButtonText: {
+ color: 'white',
+ fontSize: 12,
+ },
text: {
fontSize: 24,
fontWeight: 'bold',
+ textAlign: 'center',
},
button: {
borderRadius: 50,
diff --git a/screens/HomeScreen.js b/screens/HomeScreen.js
index 88a7a43..81648e9 100644
--- a/screens/HomeScreen.js
+++ b/screens/HomeScreen.js
@@ -1,29 +1,195 @@
-import React from 'react';
-import { View, Text, StyleSheet, Dimensions } from 'react-native';
+import React, { useState, useEffect } from 'react';
+import { View, TextInput, FlatList, TouchableOpacity, Text, StyleSheet, Alert } 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';
+
+const HomeScreen = () => {
+ const [from, setFrom] = useState('');
+ const [to, setTo] = useState('');
+ const [stops, setStops] = useState([]);
+ const [filteredStops, setFilteredStops] = useState([]);
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ const fetchStops = async () => {
+ const { data, error } = await supabase.from('stops').select('*');
+ if (error) {
+ Alert.alert('Error', 'Failed to fetch stops.');
+ return;
+ }
+ setStops(data);
+ };
+ fetchStops();
+ }, []);
+
+ const handleInputChange = (text, type) => {
+ const filtered = stops.filter((stop) =>
+ stop.name.toLowerCase().includes(text.toLowerCase())
+ );
+ setFilteredStops(filtered);
+
+ if (type === 'from') setFrom(text);
+ else setTo(text);
+ };
+
+ const handleSelectStop = (stop, type) => {
+ if (type === 'from') setFrom(stop.name);
+ else setTo(stop.name);
+ setFilteredStops([]);
+ };
+
+ const handleSearch = async () => {
+ if (!from || !to) {
+ Alert.alert('Error', 'Please enter both "From" and "To" stops.');
+ return;
+ }
+
+ const fromStop = stops.find((stop) => stop.name.toLowerCase() === from.toLowerCase());
+ const toStop = stops.find((stop) => stop.name.toLowerCase() === to.toLowerCase());
+
+ if (!fromStop || !toStop) {
+ Alert.alert('Error', 'One or both stops not found.');
+ return;
+ }
+
+ if (fromStop.route_id !== toStop.route_id) {
+ Alert.alert('Error', 'The selected stops are not on the same route.');
+ return;
+ }
+
+ if (fromStop.stop_order >= toStop.stop_order) {
+ Alert.alert('Error', '"From" stop must come before "To" stop in the route.');
+ return;
+ }
+
+ navigation.navigate('Map', {
+ origin: { latitude: fromStop.latitude, longitude: fromStop.longitude },
+ destination: { latitude: toStop.latitude, longitude: toStop.longitude },
+ });
+ };
+
+ const handleUseMyLocation = async () => {
+ const { status } = await Location.requestForegroundPermissionsAsync();
+ if (status !== 'granted') {
+ Alert.alert('Permission Denied', 'Location permission is required to use this feature.');
+ return;
+ }
+
+ const location = await Location.getCurrentPositionAsync({});
+ const { latitude, longitude } = location.coords;
+
+ let nearestStop = null;
+ let minDistance = Infinity;
+
+ stops.forEach((stop) => {
+ const distance = haversine(
+ { latitude, longitude },
+ { latitude: stop.latitude, longitude: stop.longitude }
+ );
+ if (distance < minDistance) {
+ minDistance = distance;
+ nearestStop = stop;
+ }
+ });
+
+ if (nearestStop) {
+ setFrom(nearestStop.name);
+ Alert.alert('Nearest Stop', `Nearest stop is ${nearestStop.name}`);
+ }
+ };
-const HomeScreen = (navigation) => {
return (
+
+ handleInputChange(text, 'from')}
+ />
+ {filteredStops.length > 0 && (
+ item.id.toString()}
+ renderItem={({ item }) => (
+ handleSelectStop(item, 'from')}
+ >
+ {item.name}
+
+ )}
+ />
+ )}
+ handleInputChange(text, 'to')}
+ />
+ {filteredStops.length > 0 && (
+ item.id.toString()}
+ renderItem={({ item }) => (
+ handleSelectStop(item, 'to')}
+ >
+ {item.name}
+
+ )}
+ />
+ )}
+
+ Use My Location
+
+
+ Find Route
+
);
};
const styles = StyleSheet.create({
- safeArea: {
- flex: 1,
- backgroundColor: '#dce2ef',
- },
-
container: {
flex: 1,
- justifyContent: 'center',
+ padding: 20,
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 15,
+ padding: 15,
+ marginBottom: 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,
},
- text: {
- fontSize: 24,
+ buttonText: {
+ color: 'white',
+ fontSize: 16,
fontWeight: 'bold',
},
});
diff --git a/screens/LoginScreen.js b/screens/LoginScreen.js
index d5b7ca5..6b33e07 100644
--- a/screens/LoginScreen.js
+++ b/screens/LoginScreen.js
@@ -1,11 +1,100 @@
-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 } 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';
const LoginScreen = () => {
const navigation = useNavigation();
const fadeAnim = useRef(new Animated.Value(0)).current;
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+
+ // Function to handle user login
+ const signInWithEmail = async () => {
+ const { error } = await supabase.auth.signInWithPassword({ email, password });
+ if (error) {
+ Alert.alert('Login Failed', error.message);
+ } else {
+ Alert.alert('Login Successful', 'Welcome back!');
+ navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator' }] }); // Navigate to the main app
+ }
+ };
+
+ // 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' }] });
+ }
+ }
+ };
+
+ // 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,52 +107,82 @@ const LoginScreen = () => {
useNativeDriver: true,
});
- Animated.loop(
- Animated.sequence([fadeIn, fadeOut])
- ).start();
+ Animated.loop(Animated.sequence([fadeIn, fadeOut])).start();
}, []);
return (
LoetoLink
+
-
Welcome!
-
-
- Sign up with Apple
+
+
+
+ Sign In
-
-
- Sign up with Google
-
- navigation.navigate('Login')}>
- I already have an account
+
+ Sign Up
-
+
+
+
+
+
+
+
+
+
navigation.reset({ index: 0, routes: [{ name: 'MainTabNavigator', params: { screen: 'Map' } }], })}>
+ style={styles.guestButton}
+ onPress={() =>
+ 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.
+ {}}>
+ Terms of Service
+
+ ,{' '}
+ {}}>
+ Bus/Combi Policy
+ {' '}
+ and good behavior in the bus/combi.
);
@@ -79,35 +198,34 @@ const styles = StyleSheet.create({
},
header: {
position: 'absolute',
- top: 60,
+ top: 20,
left: 20,
- alignSelf: 'flex-start',
+ flexDirection: 'row',
+ alignItems: 'center',
},
logoText: {
fontSize: 24,
fontWeight: 'bold',
+ marginRight: 10,
+ },
+ splashImage: {
+ width: 40,
+ height: 40,
+ resizeMode: 'contain',
},
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 +235,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 +253,8 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
- googleButton: {
- backgroundColor: '#fff',
+ newButton: {
+ backgroundColor: '#808080',
padding: 10,
borderRadius: 25,
width: '100%',
@@ -151,15 +270,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 +299,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 +315,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 +345,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..7dcda3b 100644
--- a/screens/MapViewScreen.js
+++ b/screens/MapViewScreen.js
@@ -1,55 +1,89 @@
-import React from 'react';
-import { View, StyleSheet, Dimensions } from 'react-native';
-import MapView, { Marker } from 'react-native-maps';
+import React, { useState, useEffect } from 'react';
+import { View, StyleSheet, Dimensions, Text } from 'react-native';
+import MapView, { Marker, Polyline } 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,
- },
-];
+const MapViewScreen = ({ route }) => {
+ const { origin, destination } = route.params || {}; // Fallback to undefined if params are missing
+
+ const [routeCoordinates, setRouteCoordinates] = useState([]);
+
+ useEffect(() => {
+ if (origin && destination) {
+ const fetchRoute = async () => {
+ try {
+ const directionsUrl = `https://maps.gomaps.pro/maps/api/directions/json?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&key=AlzaSykD0-TOgCvku5D5nyYC67DmWk2aaon-COn`;
+ const response = await fetch(directionsUrl);
+ const directionsData = await response.json();
+
+ if (directionsData.status === 'OK') {
+ const points = decodePolyline(directionsData.routes[0].overview_polyline.points);
+ setRouteCoordinates(points);
+ } else {
+ console.error('Failed to fetch directions:', directionsData.status);
+ }
+ } catch (error) {
+ console.error('Error fetching route:', error);
+ }
+ };
+
+ fetchRoute();
+ }
+ }, [origin, destination]);
+
+ 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;
+ };
-const MapViewScreen = () => {
return (
- {/* Central Marker */}
-
+ {/* Render markers if origin and destination are provided */}
+ {origin && }
+ {destination && }
- {/* Taxi Rank Markers */}
- {taxiRanks.map((rank) => (
- 0 && (
+
- ))}
+ )}
);
@@ -58,11 +92,17 @@ const MapViewScreen = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
},
map: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
+ errorText: {
+ fontSize: 16,
+ color: 'red',
+ },
});
export default MapViewScreen;
\ No newline at end of file
diff --git a/screens/MostUsedScreen.js b/screens/MostUsedScreen.js
new file mode 100644
index 0000000..4b631ac
--- /dev/null
+++ b/screens/MostUsedScreen.js
@@ -0,0 +1,218 @@
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, Animated, Dimensions, TouchableWithoutFeedback } from 'react-native';
+import MapView, { Marker } from 'react-native-maps';
+import { useNavigation } from '@react-navigation/native';
+
+const MostUsedScreen = () => {
+ const navigation = useNavigation();
+ const [expandedId, setExpandedId] = useState(null);
+ const [animations, setAnimations] = useState({});
+
+const mostUsed = [
+ {
+ id: '1',
+ name: 'Route 1 & 2',
+ description: 'Popular commuter route through downtown',
+ coordinates: {
+ latitude: -24.6581, // Gaborone latitude
+ longitude: 25.9122, // Gaborone longitude
+ }
+ },
+ {
+ id: '2',
+ name: 'Route 3 & 4',
+ description: 'Express route to shopping district',
+ coordinates: {
+ latitude: -24.6581, // Gaborone latitude
+ longitude: 25.9122, // Gaborone longitude
+ }
+ },
+];
+
+ useEffect(() => {
+ const initialAnimations = {};
+ mostUsed.forEach(item => {
+ initialAnimations[item.id] = new Animated.Value(0);
+ });
+ setAnimations(initialAnimations);
+ }, []);
+
+ const toggleExpand = (id) => {
+ if (!animations[id]) return;
+
+ setExpandedId(expandedId === id ? null : id);
+ Animated.timing(animations[id], {
+ toValue: expandedId === id ? 0 : 1,
+ duration: 300,
+ useNativeDriver: false,
+ }).start();
+ };
+
+ const handleBackgroundPress = () => {
+ if (expandedId && animations[expandedId]) {
+ Animated.timing(animations[expandedId], {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: false,
+ }).start(() => setExpandedId(null));
+ }
+ };
+
+ const renderItem = ({ item }) => {
+ const isExpanded = expandedId === item.id;
+ const animation = animations[item.id];
+
+ if (!animation) return null;
+
+ return (
+ {
+ e.stopPropagation();
+ toggleExpand(item.id);
+ }}
+ >
+
+ {item.name}
+
+ {item.description}
+
+
+
+ {
+ e.stopPropagation();
+ navigation.navigate('Map', { routeId: item.id });
+ }}
+ >
+ View in Map
+
+
+
+
+ );
+ };
+
+ return (
+
+
+ Most Used Screen
+ item.id}
+ key={'single-column'}
+ numColumns={1}
+ renderItem={renderItem}
+ contentContainerStyle={styles.listContent}
+ style={styles.list}
+ />
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#dce2ef',
+ padding: 20,
+ },
+ text: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 20,
+ },
+ list: {
+ width: '100%',
+ },
+ card: {
+ alignContent: 'center',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '98%',
+ margin: 10,
+ padding: 25,
+ backgroundColor: 'white',
+ borderRadius: 30,
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.25,
+ shadowRadius: 3.84,
+ elevation: 5,
+ alignSelf: 'center',
+ overflow: 'hidden',
+ },
+ cardText: {
+ fontSize: 22,
+ fontWeight: '500',
+ },
+ listContent: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 10,
+ width: '100%',
+ },
+ expandedContent: {
+ width: '100%',
+ marginTop: 20,
+ alignItems: 'center',
+ overflow: 'hidden',
+ },
+ description: {
+ fontSize: 18,
+ marginBottom: 15,
+ textAlign: 'center',
+ },
+ miniMap: {
+ width: '100%',
+ height: 200,
+ borderRadius: 15,
+ marginBottom: 15,
+ },
+ viewMapButton: {
+ backgroundColor: '#4a90e2',
+ padding: 10,
+ borderRadius: 15,
+ width: '50%',
+ alignItems: 'center',
+ },
+ viewMapButtonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
+
+export default MostUsedScreen;
\ No newline at end of file
diff --git a/screens/ProfileScreen.js b/screens/ProfileScreen.js
index 4cde952..a4a3996 100644
--- a/screens/ProfileScreen.js
+++ b/screens/ProfileScreen.js
@@ -1,31 +1,169 @@
-import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native';
+import React, { useState, useRef } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, Alert, Modal, Animated, TouchableWithoutFeedback, TextInput } from 'react-native';
+import UserHeader from '../components/UserHeader';
+import { MaterialIcons } from '@expo/vector-icons';
+import { supabase } from '../lib/supabase';
const ProfileScreen = ({ navigation }) => {
- const Profile = []
+ const [showDropdown, setShowDropdown] = useState(false);
+ const [isEditingUsername, setIsEditingUsername] = useState(false);
+ const [username, setUsername] = useState('CurrentUsername'); // Replace with actual username from user data
+ const dropdownHeight = useRef(new Animated.Value(0)).current;
+ const dropdownOpacity = useRef(new Animated.Value(0)).current;
+
+ const toggleDropdown = () => {
+ const toValue = showDropdown ? 0 : 200;
+ const opacityValue = showDropdown ? 0 : 1;
+ setShowDropdown(!showDropdown);
+
+ Animated.parallel([
+ Animated.spring(dropdownHeight, {
+ toValue,
+ useNativeDriver: false,
+ friction: 8,
+ tension: 40,
+ }),
+ Animated.timing(dropdownOpacity, {
+ toValue: opacityValue,
+ duration: 300,
+ useNativeDriver: false,
+ }),
+ ]).start();
+ };
+
+ const handleLogout = () => {
+ Alert.alert(
+ 'Logout',
+ 'Are you sure you want to logout?',
+ [
+ {
+ text: 'Cancel',
+ onPress: () => console.log('Logout canceled'),
+ style: 'cancel',
+ },
+ {
+ text: 'Logout',
+ onPress: () => navigation.replace('Login'), // Replace with your login screen name
+ },
+ ],
+ { cancelable: true }
+ );
+ };
+
+ const handleOptionSelect = (option) => {
+ toggleDropdown();
+ switch (option) {
+ case 'new':
+ // Add logic for new profile picture
+ break;
+ case 'username':
+ setIsEditingUsername(true); // Enable editing mode
+ break;
+ case 'email':
+ // Add logic for changing email
+ break;
+ case 'password':
+ // Add logic for changing password
+ break;
+ }
+ };
+
+ const handleSaveUsername = async () => {
+ try {
+ console.log('Attempting to update username:', username); // Debug log
+
+ // Update the display name in the authentication table
+ const { data, error } = await supabase.auth.updateUser({
+ data: { display_name: username }, // Update the display name field
+ });
+
+ if (error) {
+ console.error('Error updating username:', error.message);
+ Alert.alert('Error', 'Failed to update username. Please try again.');
+ return;
+ }
+
+ console.log('Username updated successfully:', data);
+ Alert.alert('Success', 'Username updated successfully!');
+ setIsEditingUsername(false); // Exit editing mode
+
+ // Fetch user data to verify the update
+ const { data: userData, error: fetchError } = await supabase.auth.getUser();
+ if (fetchError) {
+ console.error('Error fetching user data:', fetchError.message);
+ } else {
+ console.log('Updated user data:', userData);
+ }
+ } catch (err) {
+ console.error('Unexpected error:', err);
+ Alert.alert('Error', 'An unexpected error occurred. Please try again.');
+ }
+ };
+
return (
-
- Profile Screen
-
- {}}>
- Edit Information
-
-
- {}}>
- Change Theme
-
-
- {}}>
- Change Language
-
-
- {}}>
- Logout
-
-
-
+ showDropdown && toggleDropdown()}>
+
+
+
+
+
+
+
+
+
+
+
+ handleOptionSelect('new')}>
+ New Profile Picture
+
+ handleOptionSelect('username')}>
+ Edit Username
+
+ handleOptionSelect('email')}>
+ Change Email
+
+ handleOptionSelect('password')}>
+ Change Password
+
+
+
+
+ {isEditingUsername && (
+
+
+
+ Save
+
+
+ )}
+
+ {}}>
+ Change Theme
+
+
+
+ Logout
+
+
+
+
);
-}
+};
const styles = StyleSheet.create({
container: {
@@ -35,10 +173,77 @@ const styles = StyleSheet.create({
backgroundColor: '#dce2ef',
padding: 20,
},
- text: {
- fontSize: 24,
- fontWeight: 'bold',
+ contentContainer: {
+ width: '100%',
+ alignItems: 'center',
+ },
+ profilePictureContainer: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ overflow: 'hidden',
marginBottom: 30,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#fff',
+ position: 'relative',
+ },
+ editIconContainer: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(77, 77, 77, 0.17)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ dropdown: {
+ width: '100%',
+ backgroundColor: 'white',
+ borderRadius: 25,
+ padding: 5,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.25,
+ shadowRadius: 3.84,
+ elevation: 5,
+ overflow: 'hidden',
+ },
+ dropdownItem: {
+ padding: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: '#eee',
+ },
+ dropdownText: {
+ fontSize: 16,
+ color: '#333',
+ },
+ editUsernameContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginVertical: 10,
+ },
+ editableTextInput: {
+ flex: 1,
+ borderBottomWidth: 1,
+ borderBottomColor: '#ccc',
+ fontSize: 16,
+ color: '#333',
+ marginRight: 10,
+ },
+ saveButton: {
+ backgroundColor: '#364c84',
+ padding: 10,
+ borderRadius: 5,
+ },
+ saveButtonText: {
+ color: 'white',
+ fontSize: 14,
},
button: {
padding: 15,
diff --git a/screens/SplashScreen.js b/screens/SplashScreen.js
index e72cfad..31454b7 100644
--- a/screens/SplashScreen.js
+++ b/screens/SplashScreen.js
@@ -19,8 +19,9 @@ const SplashScreen = () => {
source={require('../assets/animations/FinalSplashScreen.json')} // Path to your JSON file
autoPlay
loop
- speed={15} //the speed
+ speed={5} //the speed
style={styles.lottieAnimation}
+ resizeMode="cover"
/>
);
diff --git a/screens/TransportInfoScreen.js b/screens/TransportInfoScreen.js
index 2521eb5..c82768f 100644
--- a/screens/TransportInfoScreen.js
+++ b/screens/TransportInfoScreen.js
@@ -25,7 +25,7 @@ const TransportInfoScreen = ({ navigation }) => {
case 'Delayed':
return '#364c84';
case 'Very late':
- return '#001F3F';
+ return 'black';
default:
return 'gray';
}
@@ -69,7 +69,7 @@ const TransportInfoScreen = ({ navigation }) => {
return (
- Your Route Information
+
item.id}