first commit
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
extends: ["expo", "prettier"],
|
||||
plugins: ["prettier"],
|
||||
rules: {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{ singleQuote: true, quoteProps: "preserve" },
|
||||
],
|
||||
},
|
||||
};
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,10 @@
|
|||
import { Stack } from "expo-router";
|
||||
|
||||
const Layout=()=> {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
export default Layout;
|
|
@ -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;
|
||||
}
|
||||
}`;
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
After Width: | Height: | Size: 280 B |
After Width: | Height: | Size: 258 B |
After Width: | Height: | Size: 516 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 528 B |
After Width: | Height: | Size: 751 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 805 B |
After Width: | Height: | Size: 1010 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 734 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 764 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 402 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 741 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 248 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 213 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 495 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,9 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
"nativewind/babel",
|
||||
],
|
||||
};
|
||||
};
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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};
|
||||
};
|
|
@ -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" });
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
);
|
|
@ -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: [],
|
||||
};
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|