first commit

master
neel 2025-03-25 17:38:42 +05:30
commit a6cae80944
95 changed files with 21304 additions and 0 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_bWVhc3VyZWQtcGFudGhlci04Mi5jbGVyay5hY2NvdW50cy5kZXYk
DATABASE_URL=postgresql://jsm_uber_owner:npg_2j5wHrsMyStB@ep-rapid-field-a1prd9ss-pooler.ap-southeast-1.aws.neon.tech/jsm_uber?sslmode=require
EXPO_PUBLIC_SERVER_URL=http://192.168.29.1:3000

10
.eslintrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
extends: ["expo", "prettier"],
plugins: ["prettier"],
rules: {
"prettier/prettier": [
"error",
{ singleQuote: true, quoteProps: "preserve" },
],
},
};

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
app-example

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# Welcome to your Expo app 👋
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.

37
app.json Normal file
View File

@ -0,0 +1,37 @@
{
"expo": {
"name": "Uber",
"slug": "uber",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash":{
"image":"./assets/images/splash.png",
"resizeMode":"cover",
"backgroundColor":"#2f80ed"
},
"newArchEnabled": true,
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "server",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
["expo-router", {"origin":"https://192.168.29.1:3000"}]
],
"experiments": {
"typedRoutes": true
}
}
}

13
app/(auth)/_layout.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Stack } from "expo-router";
const Layout=()=> {
return (
<Stack>
<Stack.Screen name="welcome" options={{ headerShown: false }} />
<Stack.Screen name="sign-up" options={{ headerShown: false }} />
<Stack.Screen name="sign-in" options={{ headerShown: false }} />
</Stack>
);
}
export default Layout;

79
app/(auth)/sign-in.tsx Normal file
View File

@ -0,0 +1,79 @@
import { ScrollView, Text, View, Image , } from "react-native";
import tw from "twrnc";
import InputField from "@/components/InputField";
import { images,icons } from "@/constants";
import { useCallback, useState } from "react";
import CustomButton from "@/components/CustomButton";
import { Link ,useRouter } from "expo-router";
import OAuth from "@/components/OAuth";
import { useSignIn } from "@clerk/clerk-expo";
const SignIn = () => {
const { signIn, setActive, isLoaded } = useSignIn();
const router = useRouter();
const [form,setForm]= useState({
email:"",
password:"",
});
const onSignInPress =useCallback( async () => {
if (!isLoaded) {
console.log("Clerk is not loaded yet.");
return;
}
console.log("signIn object:", signIn);
try {
const signInAttempt = await signIn.create({
identifier: form.email,
password :form.password ,
})
if (signInAttempt.status === "complete") {
await setActive({ session: signInAttempt.createdSessionId })
console.log("Successfully signed in! Navigating to home...");
router.push("/(root)/(tabs)/home")
} else {
console.error(JSON.stringify(signInAttempt, null, 2))
}
} catch (err:any) {
console.error("Sign-in attempt did not complete:", JSON.stringify(err, null, 2))
}
},
[isLoaded,form.email,form.password]
);
return (
<ScrollView style={tw`"flex-1 bg-white `}>
<View style={tw`flex-1 bg-white`}>
<View style={tw`relative w-full h-[250px]`}>
<Image
source={images.signUpCar}
style={tw`z-0 w-full h-[250px]`}
/>
<Text style={tw`text-2xl text-black font-JakartaSemiBold absolute bottom-5 left-5`}>Welcome</Text>
</View>
<View style={tw`p-5`}>
<InputField
label="Email"
placeholder="Enter your Email"
icon={icons.email}
value={form.email}
onChangeText={(value)=>setForm({... form, email:value})}
/>
<InputField
label="Password"
placeholder="Enter your Password"
icon={icons.lock}
secureTextEntry={true}
value={form.password}
onChangeText={(value)=>setForm({... form, password:value})}
/>
<CustomButton title="Sign In" onPress={onSignInPress} style={tw`mt-10`} />
<OAuth />
<Link href="/sign-up" style={tw`text-lg text-center text-general-200 mt-10`}>
<Text style={tw.style("text-black")}>Don't Have an Account? </Text>
<Text style={tw.style("text-primary-500")}>Sign Up</Text>
</Link>
{ /* Verification Modal */ }
</View>
</View>
</ScrollView>
);
};
export default SignIn;

134
app/(auth)/sign-up.tsx Normal file
View File

@ -0,0 +1,134 @@
import { ScrollView, Text, View, Image, Alert, } from "react-native";
import tw from "twrnc";
import InputField from "@/components/InputField";
import { images, icons } from "@/constants";
import { useState } from "react";
import CustomButton from "@/components/CustomButton";
import { Link, router } from "expo-router";
import OAuth from "@/components/OAuth";
import { useSignUp } from "@clerk/clerk-expo";
import { ReactNativeModal } from "react-native-modal";
import { fetchAPI } from "@/lib/fetch";
import { TokenCache } from "@/lib/auth";
const SignUp = () => {
const { isLoaded, signUp, setActive } = useSignUp();
const [showSuccessModal,setShowSuccessModal]= useState(false);
const [form, setForm] = useState({
name: "",
email: "",
password: "",
});
const [verification, setVerification] = useState({
state: "default",
error: "",
code: "",
})
const onSignUpPress = async () => {
if (!isLoaded){ return;}
try {
await signUp.create({
emailAddress: form.email,
password: form.password,
})
await signUp.prepareEmailAddressVerification({ strategy: "email_code" })
setVerification({
...verification,
state: "pending"
})
} catch (err:any) {
Alert.alert("Error",err.errors[0].longMessage)
}
}
const onVerifyPress = async () => {
if (!isLoaded) return;
try {
const CompleteSignUp = await signUp.attemptEmailAddressVerification({
code: verification.code,
})
if (CompleteSignUp.status === "complete") {
await fetchAPI(`${process.env.EXPO_PUBLIC_SERVER_URL}/api/users`,{
method:"POST",
headers:{"Content-Type": "application/json",
Authorization: `Bearer ${await TokenCache.getToken("__clerk_client_jwt")}`,},
body:JSON.stringify({
name:form.name,
email:form.email,
clerkId:CompleteSignUp.createdUserId,
})
})
await setActive({ session: CompleteSignUp.createdSessionId });
setVerification({ ...verification, state: "success" })
} else {
setVerification({ ...verification, error: "Verification failed.", state: "failed" })
}
} catch (err: any) {
setVerification({ ...verification, error: err.errors[0].longMessage, state: "success" })
}
}
return (
<ScrollView style={tw`flex-1 bg-white `}>
<View style={tw`flex-1 bg-white`}>
<View style={tw`relative w-full h-[250px]`}>
<Image
source={images.signUpCar}
style={tw`z-0 w-full h-[250px]`}
/>
<Text style={tw`text-2xl text-black font-JakartaSemiBold absolute bottom-5 left-5`}>Create Your Account</Text>
</View>
<View style={tw`p-5`}>
<InputField
label="Name"
placeholder="Enter your Name"
icon={icons.person}
value={form.name}
onChangeText={(value) => setForm({ ...form, name: value })}
/>
<InputField
label="Email"
placeholder="Enter your Email"
icon={icons.email}
value={form.email}
onChangeText={(value) => setForm({ ...form, email: value })}
/>
<InputField
label="Password"
placeholder="Enter your Password"
icon={icons.lock}
secureTextEntry={true}
value={form.password}
onChangeText={(value) => setForm({ ...form, password: value })}
/>
<CustomButton title="Sign Up" onPress={onSignUpPress} style={tw`mt-6`} />
<OAuth />
<Link href="/sign-in" style={tw.style("text-lg text-center text-general-200 mt-10")}>
<Text style={tw.style("text-black")}>Already Have an Account? </Text>
<Text style={tw.style("text-primary-500")}>Log In</Text>
</Link>
<ReactNativeModal isVisible={verification.state === "pending"} onModalHide={()=> {if(verification.state==="success") setShowSuccessModal(true)} } >
<View style={tw`bg-white px-7 py-9 rounded-2xl min-h-[300px]`}>
<Text style={tw`text-2xl font-JakartaExtraBold mb-2`}>Verification</Text>
<Text style={tw`font-Jakarta mb-5`}> We've sent a verification code to {form.email}</Text>
<InputField label="Code" icon={icons.lock} placeholder="12345" value={verification.code} keyboardType="numeric" onChangeText={(code)=>setVerification ({... verification,code}) }
/>
{verification.error && (
<Text style={tw`text-red-500 text-sm mt-1`}>{verification.error}</Text>
)}
<CustomButton title="Verify Email" onPress={onVerifyPress} style={[tw.style("mt-5 "),{ backgroundColor: "#38A169" }]} />
</View>
</ReactNativeModal>
<ReactNativeModal isVisible={showSuccessModal}>
<View style={tw`bg-white px-7 py-9 rounded-2xl min-h-[300px]`}>
<Image source={images.check} style={tw`w-[110px] h-[110px] mx-auto my-5 `} />
<Text style={tw`text-3xl font-JakartaBold text-center`}>Verified</Text>
<Text style={tw`text-[14px] text-gray-400 font-Jakarta text-center mt-2`}>You have successfully verified your account.</Text>
<CustomButton title="Browse Home" onPress={()=>{setShowSuccessModal(false); router.push("/(root)/(tabs)/home")}} style={tw`mt-5`} />
</View>
</ReactNativeModal>
</View>
</View>
</ScrollView>
);
};
export default SignUp;

47
app/(auth)/welcome.tsx Normal file
View File

@ -0,0 +1,47 @@
import { router } from "expo-router";
import { Text, TouchableOpacity, View, Image } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import tw from "twrnc";
import Swiper from "react-native-swiper";
import { useRef, useState } from "react";
import { onboarding } from "@/constants";
import CustomButton from "@/components/CustomButton";
const Onboarding = () => {
const swiperRef = useRef<Swiper>(null);
const [activeIndex, setActiveIndex] = useState(0);
const isLastSlide = activeIndex === onboarding.length-1;
return (
<SafeAreaView style={tw`flex h-full items-center justify-between bg-white`}>
<TouchableOpacity
onPress={() => { router.replace("/(auth)/sign-up"); }}
style={tw`w-full flex justify-end items-end p-5 `}>
<Text style={tw`text-black flex text-base font-JakartaBold `}>Skip</Text>
</TouchableOpacity>
<Swiper ref={swiperRef} loop={false}
dot={<View style={tw`w-[32px] h-[4px] mx-1 bg-[#E2E8F0] rounded-full `} />}
activeDot={
<View style={tw`w-[32px] h-[4px] mx-1 bg-[#0286FF] rounded-full `} />
}
onIndexChanged={(index) => setActiveIndex(index)}
>
{onboarding.map((item) =>
(<View key={item.id} style={tw`flex items-center justify-center p-5`}>
<Image
source={item.image}
style={tw`w-full h-[300px]`}
resizeMode="contain"
/>
<View style={tw`flex flex-row items-center justify-center w-full mt-10`}>
<Text style={tw`text-black text-3xl font-bold mx-10 text-center`}>{item.title}</Text>
</View>
<Text style={tw`text-lg font-JakartaSemiBold text-center text-[#858585] mx-10 mt-3`}>{item.description}</Text>
</View>))
}
</Swiper>
<CustomButton title={isLastSlide ? "Get Started": "Next"}
onPress={()=>isLastSlide ? router.replace("/(auth)/sign-up") : swiperRef.current ?. scrollBy(1)}
style={tw`w-11/12 mt-10`}/>
</SafeAreaView>
)
}
export default Onboarding;

View File

@ -0,0 +1,24 @@
import { icons } from "@/constants";
import { Stack, Tabs } from "expo-router";
import { Image, ImageSourcePropType, View } from "react-native";
import tw from "twrnc";
const TabIcon=({source,focused}:{source:ImageSourcePropType; focused:boolean;})=>{
return(
<View style={tw.style(`flex flex-row justify-center items-center rounded-full w-7 h-7 ${focused ? "bg-green-300 ":"" } `)}>
<View style={tw.style(`rounded-full w-12 h-12 items-center justify-center ${focused ? "bg-green-400":"" } `)}>
<Image source={source} tintColor="white" resizeMode="contain" style={tw`w-7 h-7 `} />
</View>
</View>
);
};
const Layout=()=> {
return(
<Tabs initialRouteName="home" screenOptions={{tabBarActiveTintColor :"white",tabBarInactiveTintColor:"white",tabBarShowLabel:false,tabBarStyle:{backgroundColor:"#333333",paddingBottom:25,borderRadius:50,overflow:"hidden",marginHorizontal:20,marginBottom:20,height:70,display:"flex",justifyContent:"space-between",alignItems:"center",flexDirection:"row",position:"absolute",},headerShown: false, }}>
<Tabs.Screen name="home" options={{title:"Home", tabBarIcon:({focused})=><TabIcon focused={focused} source={icons.home} /> }} />
<Tabs.Screen name="rides" options={{title:"Rides", tabBarIcon:({focused})=><TabIcon focused={focused} source={icons.list} /> }} />
<Tabs.Screen name="chat" options={{title:"Chat", tabBarIcon:({focused})=><TabIcon focused={focused} source={icons.chat} /> }} />
<Tabs.Screen name="profile" options={{title:"Profile", tabBarIcon:({focused})=><TabIcon focused={focused} source={icons.profile} /> }} />
</Tabs>
)
}
export default Layout;

View File

@ -0,0 +1,10 @@
import { Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const Chat =() => {
return(
<SafeAreaView>
<Text>Chat</Text>
</SafeAreaView>
)
}
export default Chat;

View File

@ -0,0 +1,14 @@
import { SignedIn, SignedOut, useUser } from '@clerk/clerk-expo';
import { SafeAreaView, Text, View } from 'react-native';
export default function Page() {
const { user } = useUser()
return (
<SafeAreaView>
<SignedIn>
<Text>Hello {user?.emailAddresses[0].emailAddress}</Text>
</SignedIn>
</SafeAreaView>
)
}

View File

@ -0,0 +1,10 @@
import { Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const profile =() => {
return(
<SafeAreaView>
<Text>Profile</Text>
</SafeAreaView>
)
}
export default profile;

View File

@ -0,0 +1,10 @@
import { Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const Rides =() => {
return(
<SafeAreaView>
<Text>Rides</Text>
</SafeAreaView>
)
}
export default Rides;

10
app/(root)/_layout.tsx Normal file
View File

@ -0,0 +1,10 @@
import { Stack } from "expo-router";
const Layout=()=> {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
}
export default Layout;

42
app/+html.tsx Normal file
View File

@ -0,0 +1,42 @@
import { ScrollViewStyleReset } from "expo-router/html";
import { type PropsWithChildren } from "react";
/**
* This file is web-only and used to configure the root HTML for every web page during static rendering.
* The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
*/
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

29
app/+not-found.tsx Normal file
View File

@ -0,0 +1,29 @@
import { Link, Stack } from "expo-router";
import { StyleSheet,Text,View } from "react-native";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<View style={styles.container}>
<Text type="title">This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text type="link">Go to home screen!</Text>
</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
});

48
app/_layout.tsx Normal file
View File

@ -0,0 +1,48 @@
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import "react-native-reanimated";
import { ClerkLoaded, ClerkProvider } from '@clerk/clerk-expo'
import { TokenCache } from "@/lib/auth";
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
"Jakarta-Bold": require("../assets/fonts/PlusJakartaSans-Bold.ttf"),
"Jakarta-ExtraBold": require("../assets/fonts/PlusJakartaSans-ExtraBold.ttf"),
"Jakarta-ExtraLight": require("../assets/fonts/PlusJakartaSans-ExtraLight.ttf"),
"Jakarta-Light": require("../assets/fonts/PlusJakartaSans-Light.ttf"),
"Jakarta-Medium": require("../assets/fonts/PlusJakartaSans-Medium.ttf"),
"Jakarta-Regular": require("../assets/fonts/PlusJakartaSans-Regular.ttf"),
"Jakarta-SemiBold": require("../assets/fonts/PlusJakartaSans-SemiBold.ttf"),
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={TokenCache}>
<ClerkLoaded>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(root)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</ClerkLoaded>
</ClerkProvider>
);
}

29
app/api/user+api.ts Normal file
View File

@ -0,0 +1,29 @@
import { neon } from '@neondatabase/serverless';
export async function POST(request: Request) {
try {
const sql = neon(`${process.env.DATABASE_URL}`);
const { name, email, clerkId } = await request.json()
if (!name || !email || !clerkId) {
return Response.json({ error: "Miss required fields" }, { status: 400 })
}
const response = await sql`
INSERT INTO users(
name,
email,
clerk_id
)
VALUES(
${name},
${email},
${clerkId}
)
`;
return new Response(JSON.stringify({ data: response }), { status: 201 });
}
catch (error) {
console.log(error);
return Response.json({ error: error }, { status: 500 })
}
}

11
app/index.tsx Normal file
View File

@ -0,0 +1,11 @@
import { Redirect } from "expo-router";
import { useAuth } from '@clerk/clerk-expo'
const Home =() => {
const { isSignedIn } = useAuth()
console.log("isSignedIn:", isSignedIn);
if (isSignedIn) {
return <Redirect href="/(root)/(tabs)/home" />
}
return<Redirect href="/(auth)/welcome"/>
}
export default Home;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/icons/arrow-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

BIN
assets/icons/arrow-up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

BIN
assets/icons/back-arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

BIN
assets/icons/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icons/check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

BIN
assets/icons/dollar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

BIN
assets/icons/email.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/eyecross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/icons/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
assets/icons/list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

BIN
assets/icons/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icons/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/icons/marker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
assets/icons/person.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/pin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

BIN
assets/icons/point.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/icons/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/icons/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

BIN
assets/icons/target.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/icons/to.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/images/check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/images/message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
assets/images/no-result.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

9
babel.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};

1
backend/.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgresql://jsm_uber_owner:npg_2j5wHrsMyStB@ep-rapid-field-a1prd9ss-pooler.ap-southeast-1.aws.neon.tech/jsm_uber?sslmode=require

1016
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
backend/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"pg": "^8.14.1"
}
}

37
backend/server.js Normal file
View File

@ -0,0 +1,37 @@
require("dotenv").config();
const express = require("express");
const cors = require("cors");
const { Pool } = require("pg");
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// PostgreSQL Connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }, // Required for Neon
});
// Test Route
app.get("/", (req, res) => {
res.send("Backend is running!");
});
// Example: Fetch Users
app.get("/users", async (req, res) => {
try {
const result = await pool.query("SELECT * FROM users");
res.json(result.rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Start Server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

View File

@ -0,0 +1,41 @@
import { ButtonProps } from "@/types/type";
import { TouchableOpacity, Text } from "react-native";
import tw from "twrnc";
const getBgVariantStyle = (variant: ButtonProps["bgVariant"]) => {
switch (variant) {
case "secondary":
return "bg-gray-500";
case "danger":
return "bg-red-500";
case "success":
return "bg-green-500";
case "outline":
return "bg-transparent border-neutral-300 border-[0.5px}";
default:
return "bg-[#0286ff]";
}
}
const getTextVariantStyle = (variant: ButtonProps["textVariant"]) => {
switch (variant) {
case "primary":
return "text-black";
case "secondary":
return "text-gray-100";
case "danger":
return "text-red-100";
case "success":
return "text-green-100";
default:
return "text-white";
}
}
const CustomButton = ({ onPress, title, bgVariant = "primary", textVariant = "default", IconLeft, IconRight, style, ...props }: ButtonProps) => {
return(
<TouchableOpacity onPress={onPress} style={[tw.style(`w-full p-3 rounded-full flex flex-row justify-center items-center shadow-md shadow-neutral-400/70 ${getBgVariantStyle(bgVariant)} `),typeof style === "string" ? tw.style(style) : style]}{...props} >
{IconLeft && <IconLeft />}
<Text style={tw.style(`text-lg font-bold ${getTextVariantStyle(textVariant)}`)}>{title}</Text>
{IconRight && <IconRight />}
</TouchableOpacity>
);
}
export default CustomButton;

20
components/InputField.tsx Normal file
View File

@ -0,0 +1,20 @@
import { InputFieldProps } from "@/types/type";
import { KeyboardAvoidingView, Text, TouchableWithoutFeedback, View ,Image, TextInput, Platform, Keyboard} from "react-native";
import tw from "twrnc";
const InputField=({
label,labelStyle,icon, secureTextEntry=false, containerStyle,inputStyle,iconStyle,style,...props
}:InputFieldProps)=>(
<KeyboardAvoidingView behavior={Platform.OS == "ios"? "padding":"height"}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={tw`my-2 w-full`}>
<Text style={tw.style(`text-lg font-JakartaSemiBold mb-3 ${labelStyle}`)}>{label}</Text>
<View style={tw.style(`flex flex-row justify-start items-center relative bg-neutral-100 rounded-full border-neutral-100 focus:border-primary-500 ${containerStyle}`)}>
{icon && (<Image source={icon} style={tw.style(`w-6 h-6 ml-4 ${iconStyle}`)} />
)}
<TextInput style={tw.style(`rounded-full p-4 font-JakartaSemiBold text-[15px] flex-1 ${inputStyle} text-left`)} secureTextEntry={secureTextEntry}{...props}/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
)
export default InputField;

25
components/OAuth.tsx Normal file
View File

@ -0,0 +1,25 @@
import { Text, View,Image } from "react-native";
import tw from "twrnc";
import CustomButton from "./CustomButton";
import { icons } from "@/constants";
const OAuth =()=>{
const handleGoogleSignIn =async ()=>{
}
return(
<View>
<View style={tw`flex flex-row justify-center mt-4 gap-x-3`}>
<View style={tw`flex-1 h-[1px] bg-general-100 `}/>
<Text style={tw`text-lg`}>Or</Text>
<View style={tw`flex-1 h-[1px] bg-general-100 `}/>
</View>
<CustomButton title="Log in with Google" style={tw`mt-5 w-full shadow-none`}
IconLeft={()=>(<Image source={icons.google} resizeMode="contain" style={tw`w-5 h-5 mx-2`}/>)}
bgVariant="outline"
textVariant="primary"
onPress={handleGoogleSignIn}
/>
</View>
);
};
export default OAuth;

100
constants/index.ts Normal file
View File

@ -0,0 +1,100 @@
import arrowDown from "@/assets/icons/arrow-down.png";
import arrowUp from "@/assets/icons/arrow-up.png";
import backArrow from "@/assets/icons/back-arrow.png";
import chat from "@/assets/icons/chat.png";
import checkmark from "@/assets/icons/check.png";
import close from "@/assets/icons/close.png";
import dollar from "@/assets/icons/dollar.png";
import email from "@/assets/icons/email.png";
import eyecross from "@/assets/icons/eyecross.png";
import google from "@/assets/icons/google.png";
import home from "@/assets/icons/home.png";
import list from "@/assets/icons/list.png";
import lock from "@/assets/icons/lock.png";
import map from "@/assets/icons/map.png";
import marker from "@/assets/icons/marker.png";
import out from "@/assets/icons/out.png";
import person from "@/assets/icons/person.png";
import pin from "@/assets/icons/pin.png";
import point from "@/assets/icons/point.png";
import profile from "@/assets/icons/profile.png";
import search from "@/assets/icons/search.png";
import selectedMarker from "@/assets/icons/selected-marker.png";
import star from "@/assets/icons/star.png";
import target from "@/assets/icons/target.png";
import to from "@/assets/icons/to.png";
import check from "@/assets/images/check.png";
import getStarted from "@/assets/images/get-started.png";
import message from "@/assets/images/message.png";
import noResult from "@/assets/images/no-result.png";
import onboarding1 from "@/assets/images/onboarding1.png";
import onboarding2 from "@/assets/images/onboarding2.png";
import onboarding3 from "@/assets/images/onboarding3.png";
import signUpCar from "@/assets/images/signup-car.png";
export const images = {
onboarding1,
onboarding2,
onboarding3,
getStarted,
signUpCar,
check,
noResult,
message,
};
export const icons = {
arrowDown,
arrowUp,
backArrow,
chat,
checkmark,
close,
dollar,
email,
eyecross,
google,
home,
list,
lock,
map,
marker,
out,
person,
pin,
point,
profile,
search,
selectedMarker,
star,
target,
to,
};
export const onboarding = [
{
id: 1,
title: "The perfect ride is just a tap away!",
description:
"Your journey begins with Ryde. Find your ideal ride effortlessly.",
image: images.onboarding1,
},
{
id: 2,
title: "Best car in your hands with Ryde",
description:
"Discover the convenience of finding your perfect ride with Ryde",
image: images.onboarding2,
},
{
id: 3,
title: "Your ride, your way. Let's go!",
description:
"Enter your destination, sit back, and let us take care of the rest.",
image: images.onboarding3,
},
];
export const data = {
onboarding,
};

27
lib/auth.ts Normal file
View File

@ -0,0 +1,27 @@
import * as SecureStore from "expo-secure-store";
export const TokenCache = {
getToken: async (key: string) => {
try {
const item = await SecureStore.getItemAsync(key);
if (item) {
console.log(`${key} was used 🔐`);
} else {
console.log("No values stored under key:", key);
}
return item;
} catch (error) {
console.error("Secure Store getItem error:", error);
await SecureStore.deleteItemAsync(key); // Delete potentially corrupted value
return null;
}
},
saveToken: async (key: string, token: string) => {
try {
await SecureStore.setItemAsync(key, token);
console.log(`${key} has been saved successfully ✅`);
} catch (error) {
console.error("Secure Store saveToken error:", error);
}
},
};

39
lib/fetch.ts Normal file
View File

@ -0,0 +1,39 @@
import {useState, useEffect, useCallback} from "react";
export const fetchAPI = async (url: string, options?: RequestInit) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Fetch error:", error);
throw error;
}
};
export const useFetch = <T>(url: string, options?: RequestInit) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await fetchAPI(url, options);
setData(result.data);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
return {data, loading, error, refetch: fetchData};
};

6
metro.config.js Normal file
View File

@ -0,0 +1,6 @@
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: "./global.css" });

3
nativewind-env.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

18872
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "uber-clone",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"@clerk/clerk-expo": "^2.9.0",
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "^14.0.2",
"@neondatabase/serverless": "^0.10.4",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"expo": "~52.0.39",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.8",
"expo-font": "~13.0.4",
"expo-haptics": "~14.0.1",
"expo-linking": "~7.0.5",
"expo-local-authentication": "^15.0.2",
"expo-router": "~4.0.19",
"expo-secure-store": "^14.0.1",
"expo-splash-screen": "~0.29.22",
"expo-status-bar": "~2.0.1",
"expo-symbols": "~0.2.2",
"expo-system-ui": "~4.0.8",
"expo-web-browser": "~14.0.2",
"nativewind": "^4.1.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native-animatable": "^1.4.0",
"react-native-gesture-handler": "~2.20.2",
"react-native-modal": "^14.0.0-rc.1",
"react-native-reanimated": "3.16.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-swiper": "^1.6.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"tailwindcss": "^3.4.17",
"twrnc": "^4.6.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/jest": "^29.5.12",
"@types/react-test-renderer": "^18.3.0",
"eslint-config-expo": "~8.0.1",
"jest": "^29.2.1",
"jest-expo": "~52.0.6",
"react-test-renderer": "18.3.1"
},
"private": true
}

112
scripts/reset-project.js Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* This script is used to reset the project to a blank state.
* It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
* You can remove the `reset-project` script from package.json and safely delete this file after running it.
*/
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const root = process.cwd();
const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
const exampleDir = "app-example";
const newAppDir = "app";
const exampleDirPath = path.join(root, exampleDir);
const indexContent = `import { Text, View } from "react-native";
export default function Index() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Edit app/index.tsx to edit this screen.</Text>
</View>
);
}
`;
const layoutContent = `import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}
`;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const moveDirectories = async (userInput) => {
try {
if (userInput === "y") {
// Create the app-example directory
await fs.promises.mkdir(exampleDirPath, { recursive: true });
console.log(`📁 /${exampleDir} directory created.`);
}
// Move old directories to new app-example directory or delete them
for (const dir of oldDirs) {
const oldDirPath = path.join(root, dir);
if (fs.existsSync(oldDirPath)) {
if (userInput === "y") {
const newDirPath = path.join(root, exampleDir, dir);
await fs.promises.rename(oldDirPath, newDirPath);
console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`);
} else {
await fs.promises.rm(oldDirPath, { recursive: true, force: true });
console.log(`❌ /${dir} deleted.`);
}
} else {
console.log(`➡️ /${dir} does not exist, skipping.`);
}
}
// Create new /app directory
const newAppDirPath = path.join(root, newAppDir);
await fs.promises.mkdir(newAppDirPath, { recursive: true });
console.log("\n📁 New /app directory created.");
// Create index.tsx
const indexPath = path.join(newAppDirPath, "index.tsx");
await fs.promises.writeFile(indexPath, indexContent);
console.log("📄 app/index.tsx created.");
// Create _layout.tsx
const layoutPath = path.join(newAppDirPath, "_layout.tsx");
await fs.promises.writeFile(layoutPath, layoutContent);
console.log("📄 app/_layout.tsx created.");
console.log("\n✅ Project reset complete. Next steps:");
console.log(
`1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
userInput === "y"
? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
: ""
}`,
);
} catch (error) {
console.error(`❌ Error during script execution: ${error.message}`);
}
};
rl.question(
"Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
(answer) => {
const userInput = answer.trim().toLowerCase() || "y";
if (userInput === "y" || userInput === "n") {
moveDirectories(userInput).finally(() => rl.close());
} else {
console.log("❌ Invalid input. Please enter 'Y' or 'N'.");
rl.close();
}
},
);

86
tailwind.config.js Normal file
View File

@ -0,0 +1,86 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {
fontFamily: {
Jakarta: ["Jakarta", "sans-serif"],
JakartaBold: ["Jakarta-Bold", "sans-serif"],
JakartaExtraBold: ["Jakarta-ExtraBold", "sans-serif"],
JakartaExtraLight: ["Jakarta-ExtraLight", "sans-serif"],
JakartaLight: ["Jakarta-Light", "sans-serif"],
JakartaMedium: ["Jakarta-Medium", "sans-serif"],
JakartaSemiBold: ["Jakarta-SemiBold", "sans-serif"],
},
colors: {
primary: {
100: "#F5F8FF",
200: "#EBF4FF",
300: "#C3D9FF",
400: "#9BBFFF",
500: "#0286FF",
600: "#6A85E6",
700: "#475A99",
800: "#364573",
900: "#242B4D",
},
secondary: {
100: "#F8F8F8",
200: "#F1F1F1",
300: "#D9D9D9",
400: "#C2C2C2",
500: "#AAAAAA",
600: "#999999",
700: "#666666",
800: "#4D4D4D",
900: "#333333",
},
success: {
100: "#F0FFF4",
200: "#C6F6D5",
300: "#9AE6B4",
400: "#68D391",
500: "#38A169",
600: "#2F855A",
700: "#276749",
800: "#22543D",
900: "#1C4532",
},
danger: {
100: "#FFF5F5",
200: "#FED7D7",
300: "#FEB2B2",
400: "#FC8181",
500: "#F56565",
600: "#E53E3E",
700: "#C53030",
800: "#9B2C2C",
900: "#742A2A",
},
warning: {
100: "#FFFBEB",
200: "#FEF3C7",
300: "#FDE68A",
400: "#FACC15",
500: "#EAB308",
600: "#CA8A04",
700: "#A16207",
800: "#854D0E",
900: "#713F12",
},
general: {
100: "#CED1DD",
200: "#858585",
300: "#EEEEEE",
400: "#0CC25F",
500: "#F6F8FA",
600: "#E6F3FF",
700: "#EBEBEB",
800: "#ADADAD",
},
},
},
},
plugins: [],
};

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": ["./*"],
"@env": ["./expo-env.d.ts"]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}

24
types/image.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
declare module "*.png" {
const value: any;
export default value;
}
declare module "*.jpg" {
const value: any;
export default value;
}
declare module "*.jpeg" {
const value: any;
export default value;
}
declare module "*.gif" {
const value: any;
export default value;
}
declare module "*.svg" {
const value: any;
export default value;
}

139
types/type.d.ts vendored Normal file
View File

@ -0,0 +1,139 @@
import {TextInputProps, TouchableOpacityProps} from "react-native";
declare interface Driver {
driver_id: number;
first_name: string;
last_name: string;
profile_image_url: string;
car_image_url: string;
car_seats: number;
rating: number;
}
declare interface MarkerData {
latitude: number;
longitude: number;
id: number;
title: string;
profile_image_url: string;
car_image_url: string;
car_seats: number;
rating: number;
first_name: string;
last_name: string;
time?: number;
price?: string;
}
declare interface MapProps {
destinationLatitude?: number;
destinationLongitude?: number;
onDriverTimesCalculated?: (driversWithTimes: MarkerData[]) => void;
selectedDriver?: number | null;
onMapReady?: () => void;
}
declare interface Ride {
origin_address: string;
destination_address: string;
origin_latitude: number;
origin_longitude: number;
destination_latitude: number;
destination_longitude: number;
ride_time: number;
fare_price: number;
payment_status: string;
driver_id: number;
user_email: string;
created_at: string;
driver: {
first_name: string;
last_name: string;
car_seats: number;
};
}
declare interface ButtonProps extends TouchableOpacityProps {
title: string;
bgVariant?: "primary" | "secondary" | "danger" | "outline" | "success";
textVariant?: "primary" | "default" | "secondary" | "danger" | "success";
IconLeft?: React.ComponentType<any>;
IconRight?: React.ComponentType<any>;
className?: string;
}
declare interface GoogleInputProps {
icon?: string;
initialLocation?: string;
containerStyle?: string;
textInputBackgroundColor?: string;
handlePress: ({
latitude,
longitude,
address,
}: {
latitude: number;
longitude: number;
address: string;
}) => void;
}
declare interface InputFieldProps extends TextInputProps {
label: string;
icon?: any;
secureTextEntry?: boolean;
labelStyle?: string;
containerStyle?: string;
inputStyle?: string;
iconStyle?: string;
className?: string;
}
declare interface PaymentProps {
fullName: string;
email: string;
amount: string;
driverId: number;
rideTime: number;
}
declare interface LocationStore {
userLatitude: number | null;
userLongitude: number | null;
userAddress: string | null;
destinationLatitude: number | null;
destinationLongitude: number | null;
destinationAddress: string | null;
setUserLocation: ({
latitude,
longitude,
address,
}: {
latitude: number;
longitude: number;
address: string;
}) => void;
setDestinationLocation: ({
latitude,
longitude,
address,
}: {
latitude: number;
longitude: number;
address: string;
}) => void;
}
declare interface DriverStore {
drivers: MarkerData[];
selectedDriver: number | null;
setSelectedDriver: (driverId: number) => void;
setDrivers: (drivers: MarkerData[]) => void;
clearSelectedDriver: () => void;
}
declare interface DriverCardProps {
item: MarkerData;
selected: number;
setSelected: () => void;
}