This repository contains the code corresponding to an in-depth tutorial available on our YouTube
channel, JavaScript Mastery.
If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects
like these step-by-step in a beginner-friendly manner!
Built with React Native for handling the user interface, Google Maps for rendering maps with directions, stripe for
handling payments, serverless Postgres for managing databases, and styled with TailwindCSS, Uber Clone is a perfect
mobile app. The primary goal is to demonstrate how to develop full-stack mobile applications to showcase the developer's
skills in a unique manner that creates a lasting impact.
If you're getting started and need assistance or face any bugs, join our active Discord community with over 34k+
members. It's a place where people help each other out.
Follow these steps to set up the project locally on your machine.
Replace the placeholder values with your actual Clerk, Stripe, NeonDB, Google Maps, andgeoapify credentials. You can
obtain these credentials by signing up on
the Clerk, Stripe, NeonDB, Google Maps
and geoapify websites respectively.
Here are some code snippets from the project to help you get started quickly.
.vscode/settings.json
{
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
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: [],
};
types/type.d.ts
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;
}
types/image.d.ts
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;
}
constants/index.ts
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,
};
Root Layout Fonts
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"),
});
components/CustomButton
import {TouchableOpacity, Text} from "react-native";
import {ButtonProps} from "@/types/type";
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,
className,
...props
}: ButtonProps) => {
return (
<TouchableOpacity
onPress={onPress}
className={`w-full rounded-full p-3 flex flex-row justify-center items-center shadow-md shadow-neutral-400/70 ${getBgVariantStyle(bgVariant)} ${className}`}
{...props}
>
{IconLeft && <IconLeft/>}
<Text className={`text-lg font-bold ${getTextVariantStyle(textVariant)}`}>
{title}
</Text>
{IconRight && <IconRight/>}
</TouchableOpacity>
);
};
export default CustomButton;
components/InputField
import {
TextInput,
View,
Text,
Image,
KeyboardAvoidingView,
TouchableWithoutFeedback,
Keyboard,
Platform,
} from "react-native";
import {InputFieldProps} from "@/types/type";
const InputField = ({
label,
icon,
secureTextEntry = false,
labelStyle,
containerStyle,
inputStyle,
iconStyle,
className,
...props
}: InputFieldProps) => {
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View className="my-2 w-full">
<Text className={`text-lg font-JakartaSemiBold mb-3 ${labelStyle}`}>
{label}
</Text>
<View
className={`flex flex-row justify-start items-center relative bg-neutral-100 rounded-full border border-neutral-100 focus:border-primary-500 ${containerStyle}`}
>
{icon && (
<Image source={icon} className={`w-6 h-6 ml-4 ${iconStyle}`}/>
)}
<TextInput
className={`rounded-full p-4 font-JakartaSemiBold text-[15px] flex-1 ${inputStyle} text-left`}
secureTextEntry={secureTextEntry}
{...props}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
};
export default InputField;
components/DriverCard.tsx
import React from "react";
import {Image, Text, TouchableOpacity, View} from "react-native";
import {icons} from "@/constants";
import {formatTime} from "@/lib/utils";
import {DriverCardProps} from "@/types/type";
const DriverCard = ({item, selected, setSelected}: DriverCardProps) => {
return (
<TouchableOpacity
onPress={setSelected}
className={`${
selected === item.id ? "bg-general-600" : "bg-white"
} flex flex-row items-center justify-between py-5 px-3 rounded-xl`}
>
<Image
source={{uri: item.profile_image_url}}
className="w-14 h-14 rounded-full"
/>
<View className="flex-1 flex flex-col items-start justify-center mx-3">
<View className="flex flex-row items-center justify-start mb-1">
<Text className="text-lg font-JakartaRegular">{item.title}</Text>
<View className="flex flex-row items-center space-x-1 ml-2">
<Image source={icons.star} className="w-3.5 h-3.5"/>
<Text className="text-sm font-JakartaRegular">4</Text>
</View>
</View>
<View className="flex flex-row items-center justify-start">
<View className="flex flex-row items-center">
<Image source={icons.dollar} className="w-4 h-4"/>
<Text className="text-sm font-JakartaRegular ml-1">
${item.price}
</Text>
</View>
<Text className="text-sm font-JakartaRegular text-general-800 mx-1">
|
</Text>
<Text className="text-sm font-JakartaRegular text-general-800">
{formatTime(item.time!)}
</Text>
<Text className="text-sm font-JakartaRegular text-general-800 mx-1">
|
</Text>
<Text className="text-sm font-JakartaRegular text-general-800">
{item.car_seats} seats
</Text>
</View>
</View>
<Image
source={{uri: item.car_image_url}}
className="h-14 w-14"
resizeMode="contain"
/>
</TouchableOpacity>
);
};
export default DriverCard;
lib/fetch.ts
import {useState, useEffect, useCallback} from "react";
export const fetchAPI = async (url: string, options?: RequestInit) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
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};
};
lib/map.ts
import {Driver, MarkerData} from "@/types/type";
const directionsAPI = process.env.EXPO_PUBLIC_GOOGLE_API_KEY;
export const generateMarkersFromData = ({
data,
userLatitude,
userLongitude,
}: {
data: Driver[];
userLatitude: number;
userLongitude: number;
}): MarkerData[] => {
return data.map((driver) => {
const latOffset = (Math.random() - 0.5) * 0.01; // Random offset between -0.005 and 0.005
const lngOffset = (Math.random() - 0.5) * 0.01; // Random offset between -0.005 and 0.005
return {
latitude: userLatitude + latOffset,
longitude: userLongitude + lngOffset,
title: `${driver.first_name} ${driver.last_name}`,
...driver,
};
});
};
export const calculateRegion = ({
userLatitude,
userLongitude,
destinationLatitude,
destinationLongitude,
}: {
userLatitude: number | null;
userLongitude: number | null;
destinationLatitude?: number | null;
destinationLongitude?: number | null;
}) => {
if (!userLatitude || !userLongitude) {
return {
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
};
}
if (!destinationLatitude || !destinationLongitude) {
return {
latitude: userLatitude,
longitude: userLongitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
};
}
const minLat = Math.min(userLatitude, destinationLatitude);
const maxLat = Math.max(userLatitude, destinationLatitude);
const minLng = Math.min(userLongitude, destinationLongitude);
const maxLng = Math.max(userLongitude, destinationLongitude);
const latitudeDelta = (maxLat - minLat) * 1.3; // Adding some padding
const longitudeDelta = (maxLng - minLng) * 1.3; // Adding some padding
const latitude = (userLatitude + destinationLatitude) / 2;
const longitude = (userLongitude + destinationLongitude) / 2;
return {
latitude,
longitude,
latitudeDelta,
longitudeDelta,
};
};
export const calculateDriverTimes = async ({
markers,
userLatitude,
userLongitude,
destinationLatitude,
destinationLongitude,
}: {
markers: MarkerData[];
userLatitude: number | null;
userLongitude: number | null;
destinationLatitude: number | null;
destinationLongitude: number | null;
}) => {
if (
!userLatitude ||
!userLongitude ||
!destinationLatitude ||
!destinationLongitude
)
return;
try {
const timesPromises = markers.map(async (marker) => {
const responseToUser = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${marker.latitude},${marker.longitude}&destination=${userLatitude},${userLongitude}&key=${directionsAPI}`,
);
const dataToUser = await responseToUser.json();
const timeToUser = dataToUser.routes[0].legs[0].duration.value; // Time in seconds
const responseToDestination = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${userLatitude},${userLongitude}&destination=${destinationLatitude},${destinationLongitude}&key=${directionsAPI}`,
);
const dataToDestination = await responseToDestination.json();
const timeToDestination =
dataToDestination.routes[0].legs[0].duration.value; // Time in seconds
const totalTime = (timeToUser + timeToDestination) / 60; // Total time in minutes
const price = (totalTime * 0.5).toFixed(2); // Calculate price based on time
return {...marker, time: totalTime, price};
});
return await Promise.all(timesPromises);
} catch (error) {
console.error("Error calculating driver times:", error);
}
};
lib/utils.ts
import {Ride} from "@/types/type";
export const sortRides = (rides: Ride[]): Ride[] => {
const result = rides.sort((a, b) => {
const dateA = new Date(`${a.created_at}T${a.ride_time}`);
const dateB = new Date(`${b.created_at}T${b.ride_time}`);
return dateB.getTime() - dateA.getTime();
});
return result.reverse();
};
export function formatTime(minutes: number): string {
const formattedMinutes = +minutes?.toFixed(0) || 0;
if (formattedMinutes < 60) {
return `${minutes} min`;
} else {
const hours = Math.floor(formattedMinutes / 60);
const remainingMinutes = formattedMinutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
}
export function formatDate(dateString: string): string {
const date = new Date(dateString);
const day = date.getDate();
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const month = monthNames[date.getMonth()];
const year = date.getFullYear();
return `${day < 10 ? "0" + day : day} ${month} ${year}`;
}
GET Rides SQL Query
SELECT
rides.ride_id,
rides.origin_address,
rides.destination_address,
rides.origin_latitude,
rides.origin_longitude,
rides.destination_latitude,
rides.destination_longitude,
rides.ride_time,
rides.fare_price,
rides.payment_status,
rides.created_at,
'driver', json_build_object(
'driver_id', drivers.id,
'first_name', drivers.first_name,
'last_name', drivers.last_name,
'profile_image_url', drivers.profile_image_url,
'car_image_url', drivers.car_image_url,
'car_seats', drivers.car_seats,
'rating', drivers.rating
) AS driver
FROM
rides
INNER JOIN
drivers ON rides.driver_id = drivers.id
WHERE
rides.user_email = ${id}
ORDER BY
rides.created_at DESC;
SEED Drivers Query
INSERT INTO drivers (id, first_name, last_name, profile_image_url, car_image_url, car_seats, rating)
VALUES
('1', 'James', 'Wilson', 'https://ucarecdn.com/dae59f69-2c1f-48c3-a883-017bcf0f9950/-/preview/1000x666/', 'https://ucarecdn.com/a2dc52b2-8bf7-4e49-9a36-3ffb5229ed02/-/preview/465x466/', 4, '4.80'),
('2', 'David', 'Brown', 'https://ucarecdn.com/6ea6d83d-ef1a-483f-9106-837a3a5b3f67/-/preview/1000x666/', 'https://ucarecdn.com/a3872f80-c094-409c-82f8-c9ff38429327/-/preview/930x932/', 5, '4.60'),
('3', 'Michael', 'Johnson', 'https://ucarecdn.com/0330d85c-232e-4c30-bd04-e5e4d0e3d688/-/preview/826x822/', 'https://ucarecdn.com/289764fb-55b6-4427-b1d1-f655987b4a14/-/preview/930x932/', 4, '4.70'),
('4', 'Robert', 'Green', 'https://ucarecdn.com/fdfc54df-9d24-40f7-b7d3-6f391561c0db/-/preview/626x417/', 'https://ucarecdn.com/b6fb3b55-7676-4ff3-8484-fb115e268d32/-/preview/930x932/', 4, '4.90');
CREATE Drivers Table SQL Query
CREATE TABLE drivers (
id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
profile_image_ur
