Waseel: driver app, dispatch, POI suggestions, map fixes

This commit is contained in:
Krikorios
2026-08-25 02:57:49 +03:00
parent 899ca93cd5
commit 1d84003e0a
50 changed files with 3625 additions and 625 deletions
+51 -12
View File
@@ -1,7 +1,10 @@
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { Image, type ImageSourcePropType, View } from "react-native";
import { icons } from "@/constants";
import { useT } from "@/lib/i18n";
import { useTheme } from "@/lib/theme";
const TabIcon = ({
source,
@@ -13,7 +16,7 @@ const TabIcon = ({
focused: boolean;
}) => (
<View
className={`flex flex-row justify-center items-center rounded-full ${focused && "bg-general-300"}`}
className={`flex flex-row justify-center items-center rounded-full ${focused && "bg-general-300 dark:bg-neutral-800"}`}
>
<View
className={`rounded-full w-12 h-12 items-center justify-center ${focused && "bg-general-400"}`}
@@ -29,7 +32,31 @@ const TabIcon = ({
</View>
);
const TabsLayout = () => (
// Settings uses a vector glyph (MaterialCommunityIcons "cog") instead of a PNG
// asset, so it gets its own icon renderer that matches the pill styling.
const TabIconVector = ({
name,
focused,
}: {
name: React.ComponentProps<typeof MaterialCommunityIcons>["name"];
focused: boolean;
}) => (
<View
className={`flex flex-row justify-center items-center rounded-full ${focused && "bg-general-300 dark:bg-neutral-800"}`}
>
<View
className={`rounded-full w-12 h-12 items-center justify-center ${focused && "bg-general-400"}`}
>
<MaterialCommunityIcons name={name} size={28} color="white" />
</View>
</View>
);
const TabsLayout = () => {
const { isDark } = useTheme();
const t = useT();
return (
<Tabs
initialRouteName="home"
screenOptions={{
@@ -37,7 +64,7 @@ const TabsLayout = () => (
tabBarInactiveTintColor: "white",
tabBarShowLabel: false,
tabBarStyle: {
backgroundColor: "#333",
backgroundColor: isDark ? "#0a0a0a" : "#333",
borderRadius: 50,
paddingBottom: 0,
overflow: "hidden",
@@ -55,10 +82,10 @@ const TabsLayout = () => (
<Tabs.Screen
name="home"
options={{
title: "Home",
title: t("tabs.home"),
headerShown: false,
tabBarIcon: ({ focused }) => (
<TabIcon focused={focused} source={icons.home} alt="Home" />
<TabIcon focused={focused} source={icons.home} alt={t("tabs.home")} />
),
}}
/>
@@ -66,10 +93,10 @@ const TabsLayout = () => (
<Tabs.Screen
name="rides"
options={{
title: "Rides",
title: t("tabs.rides"),
headerShown: false,
tabBarIcon: ({ focused }) => (
<TabIcon focused={focused} source={icons.list} alt="Rides" />
<TabIcon focused={focused} source={icons.list} alt={t("tabs.rides")} />
),
}}
/>
@@ -77,10 +104,10 @@ const TabsLayout = () => (
<Tabs.Screen
name="chat"
options={{
title: "Chat",
title: t("tabs.chat"),
headerShown: false,
tabBarIcon: ({ focused }) => (
<TabIcon focused={focused} source={icons.chat} alt="Chat" />
<TabIcon focused={focused} source={icons.chat} alt={t("tabs.chat")} />
),
}}
/>
@@ -88,14 +115,26 @@ const TabsLayout = () => (
<Tabs.Screen
name="profile"
options={{
title: "Profile",
title: t("tabs.profile"),
headerShown: false,
tabBarIcon: ({ focused }) => (
<TabIcon focused={focused} source={icons.profile} alt="Profile" />
<TabIcon focused={focused} source={icons.profile} alt={t("tabs.profile")} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: t("tabs.settings"),
headerShown: false,
tabBarIcon: ({ focused }) => (
<TabIconVector focused={focused} name="cog" />
),
}}
/>
</Tabs>
);
);
};
export default TabsLayout;
+13 -8
View File
@@ -2,27 +2,32 @@ import { Image, ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { images } from "@/constants";
import { useT } from "@/lib/i18n";
const Chat = () => {
const t = useT();
return (
<SafeAreaView className="flex-1 bg-white p-5">
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 p-5">
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
<Text className="text-2xl font-JakartaBold">Chat</Text>
<Text className="text-2xl font-JakartaBold text-black dark:text-white">
{t("chat.title")}
</Text>
<View className="flex-1 h-fit flex justify-center items-center">
<Image
source={images.message}
alt="message"
alt={t("chat.messageAlt")}
className="w-full h-40"
resizeMode="contain"
/>
<Text className="text-3xl font-JakartaBold mt-3">
No Messages Yet
<Text className="text-3xl font-JakartaBold mt-3 text-black dark:text-white">
{t("chat.noMessages")}
</Text>
<Text className="text-base mt-2 text-center px-7">
Start a conversation with your friends and family
<Text className="text-base mt-2 text-center px-7 text-general-200 dark:text-neutral-400">
{t("chat.startConversation")}
</Text>
</View>
</ScrollView>
@@ -30,4 +35,4 @@ const Chat = () => {
);
};
export default Chat;
export default Chat;
+23 -19
View File
@@ -16,7 +16,9 @@ import { NearbySuggestions } from "@/components/nearby-suggestions";
import { RideCard } from "@/components/ride-card";
import { ServiceSelector } from "@/components/service-selector";
import { icons, images } from "@/constants";
import { useT } from "@/lib/i18n";
import { useSession } from "@/lib/session";
import { useTheme } from "@/lib/theme";
import { useUserLocation } from "@/lib/use-user-location";
import { useLocationStore } from "@/store";
import { useFetch } from "@/lib/fetch";
@@ -27,6 +29,8 @@ const Home = () => {
(state) => state.setDestinationLocation,
);
const { signOut, user } = useSession();
const { isDark } = useTheme();
const t = useT();
const { data: recentRides, loading } = useFetch<Ride[]>("/(api)/ride/list");
const { status: locationStatus, retry: retryLocation } = useUserLocation();
@@ -47,7 +51,7 @@ const Home = () => {
};
return (
<SafeAreaView className="bg-general-500">
<SafeAreaView className="bg-general-500 dark:bg-neutral-950">
<FlatList
data={recentRides?.slice(0, 5)}
renderItem={({ item }) => <RideCard ride={item} />}
@@ -62,14 +66,14 @@ const Home = () => {
<>
<Image
source={images.noResult}
alt="No recent rides found"
alt={t("home.noRecentAlt")}
className="w-40 h-40"
resizeMode="contain"
/>
<Text className="text-sm">No recent rides found.</Text>
<Text className="text-sm text-black dark:text-white">{t("home.noRecent")}</Text>
</>
) : (
<ActivityIndicator size="small" color="#000" />
<ActivityIndicator size="small" color={isDark ? "#fff" : "#000"} />
)}
</View>
}
@@ -83,33 +87,33 @@ const Home = () => {
<>
<View className="flex flex-row items-center justify-between my-5">
<Text
className="text-base font-JakartaExtraBold"
className="text-base font-JakartaExtraBold text-black dark:text-white"
numberOfLines={1}
>
Welcome {user?.name || user?.email} 👋
{t("home.welcome", { name: user?.name || user?.email || "" })}
</Text>
<View className="flex flex-row items-center gap-x-1">
<TouchableOpacity
onPress={handleSignOut}
className="justify-center items-center w-10 h-10 rounded-full bg-white"
className="justify-center items-center w-10 h-10 rounded-full bg-white dark:bg-neutral-900"
>
<Image source={icons.out} className="w-4 h-4" alt="Logout" />
<Image source={icons.out} className="w-4 h-4" alt={t("home.logoutAlt")} />
</TouchableOpacity>
</View>
</View>
<GoogleTextInput
icon={icons.search}
containerStyles="bg-white shadow-md shadow-neutral-300"
containerStyles="bg-white dark:bg-neutral-900 shadow-md shadow-neutral-300 dark:shadow-neutral-950/40"
handlePress={handleDestinationPress}
/>
<Text className="text-xl font-JakartaBold mt-5 mb-3">
Your Current Location
<Text className="text-xl font-JakartaBold mt-5 mb-3 text-black dark:text-white">
{t("home.currentLocation")}
</Text>
<View className="w-full h-[300px] rounded-2xl overflow-hidden bg-white">
<View className="w-full h-[300px] rounded-2xl overflow-hidden bg-white dark:bg-neutral-900">
{locationStatus === "pending" || locationStatus === "granted" ? (
<>
{/* The map draws straight away on the Beirut fallback so the
@@ -117,10 +121,10 @@ const Home = () => {
<Map />
{locationStatus === "pending" ? (
<View className="absolute bottom-3 self-center flex-row items-center rounded-full bg-white/95 px-4 py-2 shadow-md shadow-neutral-400/40">
<View className="absolute bottom-3 self-center flex-row items-center rounded-full bg-white/95 dark:bg-neutral-900/95 px-4 py-2 shadow-md shadow-neutral-400/40 dark:shadow-neutral-950/40">
<ActivityIndicator size="small" color="#0286ff" />
<Text className="ml-2 text-xs font-JakartaMedium text-general-200">
Finding your location
<Text className="ml-2 text-xs font-JakartaMedium text-general-200 dark:text-neutral-400">
{t("home.findingLocation")}
</Text>
</View>
) : null}
@@ -133,8 +137,8 @@ const Home = () => {
)}
</View>
<Text className="text-xl font-JakartaBold mt-5 mb-3">
What do you need?
<Text className="text-xl font-JakartaBold mt-5 mb-3 text-black dark:text-white">
{t("home.whatNeed")}
</Text>
<ServiceSelector />
@@ -143,8 +147,8 @@ const Home = () => {
<NearbySuggestions />
</View>
<Text className="text-xl font-JakartaBold mt-5 mb-3">
Recent Rides
<Text className="text-xl font-JakartaBold mt-5 mb-3 text-black dark:text-white">
{t("home.recentRides")}
</Text>
</>
}
+16 -12
View File
@@ -3,49 +3,53 @@ import { SafeAreaView } from "react-native-safe-area-context";
import { InputField } from "@/components/input-field";
import { icons } from "@/constants";
import { useT } from "@/lib/i18n";
import { useSession } from "@/lib/session";
const Profile = () => {
const { user } = useSession();
const t = useT();
return (
<SafeAreaView className="flex-1">
<SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
<ScrollView
className="px-5"
contentContainerStyle={{ paddingBottom: 120 }}
>
<Text className="text-2xl font-JakartaBold my-5">My Profile</Text>
<Text className="text-2xl font-JakartaBold my-5 text-black dark:text-white">
{t("profile.title")}
</Text>
<View className="flex items-center justify-center my-5">
<Image
source={user?.avatarUrl ? { uri: user.avatarUrl } : icons.profile}
alt="Your Avatar"
alt={t("profile.avatarAlt")}
style={{ width: 110, height: 110, borderRadius: 110 / 2 }}
className=" rounded-full h-[110px] w-[110px] border-[3px] border-white shadow-sm shadow-neutral-300"
className=" rounded-full h-[110px] w-[110px] border-[3px] border-white dark:border-neutral-800 shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40"
/>
</View>
<View className="flex flex-col items-start justify-center bg-white rounded-lg shadow-sm shadow-neutral-300 px-5 py-3">
<View className="flex flex-col items-start justify-center bg-white dark:bg-neutral-900 rounded-lg shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40 px-5 py-3">
<View className="flex flex-col items-start justify-start w-full">
<InputField
label="First name"
placeholder={user?.name?.split(" ")[0] || "Your First name"}
label={t("profile.firstName")}
placeholder={user?.name?.split(" ")[0] || t("profile.firstNamePlaceholder")}
containerStyles="w-full mb-4"
inputStyles="p-3.5"
editable={false}
/>
<InputField
label="Last name"
placeholder={user?.name?.split(" ").slice(1).join(" ") || "Your Last name"}
label={t("profile.lastName")}
placeholder={user?.name?.split(" ").slice(1).join(" ") || t("profile.lastNamePlaceholder")}
containerStyles="w-full mb-4"
inputStyles="p-3.5"
editable={false}
/>
<InputField
label="Email"
placeholder={user?.email ?? "Your Email address"}
label={t("profile.email")}
placeholder={user?.email ?? t("profile.emailPlaceholder")}
containerStyles="w-full mb-4"
inputStyles="p-3.5"
editable={false}
@@ -57,4 +61,4 @@ const Profile = () => {
);
};
export default Profile;
export default Profile;
+14 -6
View File
@@ -4,13 +4,17 @@ import { SafeAreaView } from "react-native-safe-area-context";
import { RideCard } from "@/components/ride-card";
import { images } from "@/constants";
import { useFetch } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import { useTheme } from "@/lib/theme";
import type { Ride } from "@/types/type";
const Rides = () => {
const { data: recentRides, loading } = useFetch<Ride[]>("/(api)/ride/list");
const { isDark } = useTheme();
const t = useT();
return (
<SafeAreaView>
<SafeAreaView className="bg-general-500 dark:bg-neutral-950">
<FlatList
data={recentRides}
renderItem={({ item }) => <RideCard ride={item} />}
@@ -25,23 +29,27 @@ const Rides = () => {
<>
<Image
source={images.noResult}
alt="No recent rides found"
alt={t("rides.noRecentAlt")}
className="w-40 h-40"
resizeMode="contain"
/>
<Text className="text-sm">No recent rides found.</Text>
<Text className="text-sm text-black dark:text-white">
{t("rides.noRecent")}
</Text>
</>
) : (
<ActivityIndicator size="small" color="#000" />
<ActivityIndicator size="small" color={isDark ? "#fff" : "#000"} />
)}
</View>
}
ListHeaderComponent={
<Text className="text-2xl font-JakartaBold my-5">All rides</Text>
<Text className="text-2xl font-JakartaBold my-5 text-black dark:text-white">
{t("rides.allRides")}
</Text>
}
/>
</SafeAreaView>
);
};
export default Rides;
export default Rides;
+325
View File
@@ -0,0 +1,325 @@
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useFocusEffect } from "expo-router";
import { Alert, Linking, Platform, ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useCallback, useState } from "react";
import { SettingsRow } from "@/components/settings-row";
import {
type Lang,
type ThemeMode,
useSettingsStore,
} from "@/lib/settings";
import { useT } from "@/lib/i18n";
import { useLocationPermission } from "@/lib/use-location-permission";
import { useTheme } from "@/lib/theme";
type IconName = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
const SectionHeader = ({ title }: { title: string }) => (
<Text className="text-xs font-JakartaSemiBold uppercase tracking-wide text-general-200 dark:text-neutral-500 mt-6 mb-2 px-1">
{title}
</Text>
);
const Card = ({ children }: { children: React.ReactNode }) => (
<View className="rounded-2xl bg-white dark:bg-neutral-900 overflow-hidden shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40">
{children}
</View>
);
const Settings = () => {
const t = useT();
const { isDark } = useTheme();
const mode = useSettingsStore((state) => state.mode);
const setMode = useSettingsStore((state) => state.setMode);
const lang = useSettingsStore((state) => state.lang);
const setLang = useSettingsStore((state) => state.setLang);
const keepAwake = useSettingsStore((state) => state.keepAwake);
const setKeepAwake = useSettingsStore((state) => state.setKeepAwake);
const overlayRequested = useSettingsStore(
(state) => state.overlayRequested,
);
const setOverlayRequested = useSettingsStore(
(state) => state.setOverlayRequested,
);
const { status, refresh, openSettings } = useLocationPermission();
useFocusEffect(
useCallback(() => {
void refresh();
}, [refresh]),
);
const [expandedSafety, setExpandedSafety] = useState<string | null>(null);
const locationStatusLabel =
status === "granted"
? t("settings.maps.statusGranted")
: status === "denied"
? t("settings.maps.statusDenied")
: status === "blocked"
? t("settings.maps.statusBlocked")
: t("settings.maps.statusUnknown");
const modeLabel =
mode === "light"
? t("settings.appearance.light")
: mode === "dark"
? t("settings.appearance.dark")
: t("settings.appearance.system");
const langLabel =
lang === "en"
? t("settings.language.en")
: lang === "ar"
? t("settings.language.ar")
: t("settings.language.fr");
const callEmergency = useCallback(async () => {
try {
await Linking.openURL("tel:112");
} catch {
Alert.alert(
t("settings.safety.callFailedTitle"),
t("settings.safety.callFailedBody"),
);
}
}, [t]);
const chooseLanguage = useCallback(
(next: Lang) => {
const switchingToOrFromRTL = next === "ar" || lang === "ar";
setLang(next);
if (switchingToOrFromRTL) {
Alert.alert(
t("settings.language.rtlRestartTitle"),
t("settings.language.rtlRestartBody"),
);
}
},
[lang, setLang, t],
);
const openOverlaySettings = useCallback(async () => {
setOverlayRequested(true);
try {
await Linking.openSettings();
} catch {
// already flagged; nothing more to do
}
}, [setOverlayRequested]);
const appearanceOptions: { mode: ThemeMode; icon: IconName }[] = [
{ mode: "light", icon: "white-balance-sunny" },
{ mode: "dark", icon: "weather-night" },
{ mode: "system", icon: "theme-light-dark" },
];
const languageOptions: { lang: Lang; icon: IconName }[] = [
{ lang: "en", icon: "alpha-e-box" },
{ lang: "ar", icon: "alpha-a-box" },
{ lang: "fr", icon: "alpha-f-box" },
];
const safetyTiles: { key: string; icon: IconName; title: string; body: string }[] = [
{
key: "proactive",
icon: "shield-account",
title: t("settings.safety.proactive.title"),
body: t("settings.safety.proactive.body"),
},
{
key: "verification",
icon: "account-check",
title: t("settings.safety.verification.title"),
body: t("settings.safety.verification.body"),
},
{
key: "privacy",
icon: "lock",
title: t("settings.safety.privacy.title"),
body: t("settings.safety.privacy.body"),
},
];
return (
<SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
<ScrollView
className="px-5"
contentContainerStyle={{ paddingBottom: 120 }}
>
<Text className="text-2xl font-JakartaBold my-5 text-black dark:text-white">
{t("settings.title")}
</Text>
{/* 1. Maps & Navigation */}
<SectionHeader title={t("settings.maps.title")} />
<Card>
<SettingsRow
icon="map-marker-radius"
title={t("settings.maps.title")}
subtitle={t("settings.maps.description")}
right="value"
value={locationStatusLabel}
/>
{status !== "granted" ? (
<View className="border-t border-neutral-100 dark:border-neutral-800">
<SettingsRow
icon="cog"
title={t("settings.maps.openSettings")}
right="chevron"
onPress={openSettings}
/>
</View>
) : null}
</Card>
{/* 2. Appearance */}
<SectionHeader title={t("settings.appearance.title")} />
<Card>
<View className="px-4 py-2.5">
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
{t("settings.appearance.description")}
</Text>
</View>
{appearanceOptions.map((option, index) => (
<View
key={option.mode}
className={
index > 0
? "border-t border-neutral-100 dark:border-neutral-800"
: ""
}
>
<SettingsRow
icon={option.icon}
title={
option.mode === "light"
? t("settings.appearance.light")
: option.mode === "dark"
? t("settings.appearance.dark")
: t("settings.appearance.system")
}
right="value"
value={
mode === option.mode
? isDark
? "✓"
: "✓"
: ""
}
onPress={() => setMode(option.mode)}
/>
</View>
))}
</Card>
{/* 3. Safety */}
<SectionHeader title={t("settings.safety.title")} />
<Card>
<SettingsRow
icon="phone-in-talk"
title={t("settings.safety.call112")}
subtitle={t("settings.safety.call112Description")}
right="chevron"
danger
onPress={callEmergency}
/>
{safetyTiles.map((tile) => (
<View
key={tile.key}
className="border-t border-neutral-100 dark:border-neutral-800"
>
<SettingsRow
icon={tile.icon}
title={tile.title}
subtitle={
expandedSafety === tile.key ? undefined : tile.body
}
right="chevron"
onPress={() =>
setExpandedSafety((current) =>
current === tile.key ? null : tile.key,
)
}
/>
</View>
))}
</Card>
{/* 4. Language */}
<SectionHeader title={t("settings.language.title")} />
<Card>
<View className="px-4 py-2.5">
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
{t("settings.language.description")}
</Text>
</View>
{languageOptions.map((option, index) => (
<View
key={option.lang}
className={
index > 0
? "border-t border-neutral-100 dark:border-neutral-800"
: ""
}
>
<SettingsRow
icon={option.icon}
title={
option.lang === "en"
? t("settings.language.en")
: option.lang === "ar"
? t("settings.language.ar")
: t("settings.language.fr")
}
right="value"
value={lang === option.lang ? "✓" : ""}
onPress={() => chooseLanguage(option.lang)}
/>
</View>
))}
</Card>
{/* 5. Keep awake */}
<SectionHeader title={t("settings.keepAwake.title")} />
<Card>
<SettingsRow
icon="monitor"
title={t("settings.keepAwake.title")}
subtitle={t("settings.keepAwake.description")}
right="switch"
switchValue={keepAwake}
onSwitchChange={setKeepAwake}
/>
</Card>
{/* 6. Display over other apps (Android only) */}
{Platform.OS === "android" ? (
<>
<SectionHeader title={t("settings.overlay.title")} />
<Card>
<SettingsRow
icon="application-brackets"
title={t("settings.overlay.allow")}
subtitle={
overlayRequested
? t("settings.overlay.openedHint")
: t("settings.overlay.description")
}
right="chevron"
onPress={openOverlaySettings}
/>
</Card>
</>
) : null}
</ScrollView>
</SafeAreaView>
);
};
export default Settings;
+45 -43
View File
@@ -14,18 +14,19 @@ import { CustomButton } from "@/components/custom-button";
import { Map } from "@/components/map";
import { icons, images } from "@/constants";
import { ApiError, fetchAPI } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import { formatTime } from "@/lib/utils";
import { useLocationStore } from "@/store";
import type { Ride } from "@/types/type";
const POLL_MS = 3000;
const statusLabel: Record<string, string> = {
requested: "Finding your driver…",
accepted: "Driver assigned — heading to you",
en_route: "On your trip",
completed: "You've arrived!",
cancelled: "Ride cancelled",
const STATUS_KEY: Record<string, string> = {
requested: "bookRide.status.requested",
accepted: "bookRide.status.accepted",
en_route: "bookRide.status.enRoute",
completed: "bookRide.status.completed",
cancelled: "bookRide.status.cancelled",
};
// book-ride is now the live ride-status screen. The rider lands here after
@@ -33,6 +34,7 @@ const statusLabel: Record<string, string> = {
const BookRide = () => {
const { id } = useLocalSearchParams<{ id: string }>();
const rideId = Number(id);
const t = useT();
const setUserLocation = useLocationStore((s) => s.setUserLocation);
const setDestinationLocation = useLocationStore((s) => s.setDestinationLocation);
@@ -62,12 +64,12 @@ const BookRide = () => {
} catch (err) {
console.log("[BOOK_RIDE_LOAD]: ", err);
if (err instanceof ApiError && err.status === 404) {
setError("Ride not found.");
setError(t("bookRide.rideNotFound"));
}
} finally {
setLoading(false);
}
}, [rideId, setUserLocation, setDestinationLocation]);
}, [rideId, setUserLocation, setDestinationLocation, t]);
useEffect(() => {
void load();
@@ -92,7 +94,7 @@ const BookRide = () => {
await load();
} catch (err) {
console.log("[BOOK_RIDE_CANCEL]: ", err);
Alert.alert("Error", "Could not cancel this ride. Please try again.");
Alert.alert(t("bookRide.alertErrorTitle"), t("bookRide.alertErrorBody"));
} finally {
setCancelling(false);
}
@@ -100,7 +102,7 @@ const BookRide = () => {
if (loading) {
return (
<SafeAreaView className="flex-1 bg-white items-center justify-center">
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 items-center justify-center">
<ActivityIndicator size="large" color="#0286ff" />
</SafeAreaView>
);
@@ -108,12 +110,12 @@ const BookRide = () => {
if (error || !ride) {
return (
<SafeAreaView className="flex-1 bg-white items-center justify-center px-7">
<Text className="text-base text-general-200 text-center">
{error ?? "Could not load this ride."}
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 items-center justify-center px-7">
<Text className="text-base text-general-200 dark:text-neutral-400 text-center">
{error ?? t("bookRide.couldNotLoad")}
</Text>
<CustomButton
title="Back Home"
title={t("bookRide.backHome")}
onPress={() => router.replace("/(root)/(tabs)/home")}
className="mt-6"
/>
@@ -125,75 +127,75 @@ const BookRide = () => {
const terminal = ride.status === "completed" || ride.status === "cancelled";
return (
<SafeAreaView className="flex-1 bg-general-500">
<SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
<View className="h-[45%] bg-blue-500">
<Map />
</View>
<View className="flex-1 px-5 pt-4">
<Text className="text-2xl font-JakartaExtraBold mb-2">
{statusLabel[ride.status] ?? ride.status}
<Text className="text-2xl font-JakartaExtraBold mb-2 text-black dark:text-white">
{STATUS_KEY[ride.status] ? t(STATUS_KEY[ride.status]) : ride.status}
</Text>
{/* Searching state */}
{ride.status === "requested" ? (
<View className="items-center mt-6">
<ActivityIndicator size="large" color="#0286ff" />
<Text className="text-general-200 mt-3 text-center">
We&apos;re matching you with the nearest {ride.service} driver.
<Text className="text-general-200 dark:text-neutral-400 mt-3 text-center">
{t("bookRide.matchingDriver", { service: ride.service })}
</Text>
</View>
) : null}
{/* Driver card — shown once a driver is assigned. */}
{driver?.id ? (
<View className="bg-white rounded-2xl p-4 mt-2">
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mt-2">
<View className="flex-row items-center">
<Image
source={{ uri: driver.profile_image_url ?? undefined }}
className="w-16 h-16 rounded-full"
/>
<View className="ml-4 flex-1">
<Text className="text-lg font-JakartaSemiBold">
<Text className="text-lg font-JakartaSemiBold text-black dark:text-white">
{driver.first_name} {driver.last_name}
</Text>
<View className="flex-row items-center mt-1">
<Image source={icons.star} className="w-4 h-4" />
<Text className="ml-1 text-general-200">
{driver.rating?.toFixed(1) ?? "—"}
<Text className="ml-1 text-general-200 dark:text-neutral-400">
{driver.rating?.toFixed(1) ?? t("bookRide.ratingFallback")}
</Text>
{driver.car_model ? (
<Text className="ml-3 text-general-200">
<Text className="ml-3 text-general-200 dark:text-neutral-400">
{driver.car_model}
</Text>
) : null}
</View>
</View>
<Text className="text-xs text-general-200 capitalize">
<Text className="text-xs text-general-200 dark:text-neutral-400 capitalize">
{driver.service ?? ride.service}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mt-4">
<Image source={icons.to} className="w-4 h-4" />
<Text className="font-JakartaMedium text-sm" numberOfLines={1}>
<Text className="font-JakartaMedium text-sm text-black dark:text-white" numberOfLines={1}>
{ride.origin_address}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mt-2">
<Image source={icons.point} className="w-4 h-4" />
<Text className="font-JakartaMedium text-sm" numberOfLines={1}>
<Text className="font-JakartaMedium text-sm text-black dark:text-white" numberOfLines={1}>
{ride.destination_address}
</Text>
</View>
<View className="flex-row justify-between mt-4 pt-3 border-t border-neutral-100">
<Text className="text-general-200 text-xs">
<View className="flex-row justify-between mt-4 pt-3 border-t border-neutral-100 dark:border-neutral-800">
<Text className="text-general-200 dark:text-neutral-400 text-xs">
{ride.payment_status === "cash"
? "💵 Cash to driver"
: "💳 Paid by card"}
? t("bookRide.paymentCash")
: t("bookRide.paymentCard")}
</Text>
<Text className="font-JakartaBold text-emerald-600">
<Text className="font-JakartaBold text-emerald-600 dark:text-emerald-400">
${(ride.fare_price / 100).toFixed(2)}
</Text>
</View>
@@ -202,22 +204,22 @@ const BookRide = () => {
{/* Completed summary */}
{ride.status === "completed" ? (
<View className="bg-white rounded-2xl p-4 mt-4 items-center">
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mt-4 items-center">
<Image source={images.check} className="w-12 h-12" />
<Text className="text-lg font-JakartaBold mt-3">
Fare: ${(ride.fare_price / 100).toFixed(2)}
<Text className="text-lg font-JakartaBold mt-3 text-black dark:text-white">
{t("bookRide.fare", { fare: (ride.fare_price / 100).toFixed(2) })}
</Text>
<Text className="text-general-200 text-sm mt-1">
Trip time {formatTime(ride.ride_time)}
<Text className="text-general-200 dark:text-neutral-400 text-sm mt-1">
{t("bookRide.tripTime", { time: formatTime(ride.ride_time) })}
</Text>
</View>
) : null}
{/* Cancelled */}
{ride.status === "cancelled" ? (
<View className="bg-white rounded-2xl p-4 mt-4 items-center">
<Text className="text-general-200">
This ride was cancelled.
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mt-4 items-center">
<Text className="text-general-200 dark:text-neutral-400">
{t("bookRide.rideCancelled")}
</Text>
</View>
) : null}
@@ -225,17 +227,17 @@ const BookRide = () => {
<View className="mt-auto pt-6">
{terminal ? (
<CustomButton
title="Back Home"
title={t("bookRide.backHome")}
onPress={() => router.replace("/(root)/(tabs)/home")}
/>
) : (
<TouchableOpacity
onPress={cancel}
disabled={cancelling}
className="rounded-full py-3 bg-white items-center border border-rose-300"
className="rounded-full py-3 bg-white dark:bg-neutral-900 items-center border border-rose-300 dark:border-rose-900"
>
<Text className="font-JakartaBold text-rose-500">
{cancelling ? "Cancelling" : "Cancel Ride"}
{cancelling ? t("bookRide.cancelling") : t("bookRide.cancelRide")}
</Text>
</TouchableOpacity>
)}
+65 -41
View File
@@ -6,6 +6,7 @@ import { CustomButton } from "@/components/custom-button";
import { RideLayout } from "@/components/ride-layout";
import { SERVICES } from "@/constants/services";
import { ApiError, fetchAPI } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import { calculateTripFare } from "@/lib/map";
import { formatLBP } from "@/lib/pricing";
import { requestRide } from "@/lib/request-ride";
@@ -38,6 +39,7 @@ const ConfirmRide = () => {
} = useLocationStore();
const { service: storeService, setService } = useServiceStore();
const { user } = useSession();
const t = useT();
const service = params.service ?? storeService;
const selected = SERVICES.find((s) => s.id === service) ?? SERVICES[0];
@@ -154,11 +156,17 @@ const ConfirmRide = () => {
const request = async () => {
if (!userLatitude || !userLongitude || !destinationLatitude || !destinationLongitude) {
Alert.alert("Missing route", "Please set a pickup and destination first.");
Alert.alert(
t("confirmRide.alertMissingRouteTitle"),
t("confirmRide.alertMissingRouteBody"),
);
return;
}
if (!estimate) {
Alert.alert("No estimate", "We couldn't estimate this fare. Please try again.");
Alert.alert(
t("confirmRide.alertNoEstimateTitle"),
t("confirmRide.alertNoEstimateBody"),
);
return;
}
@@ -194,8 +202,8 @@ const ConfirmRide = () => {
const msg =
err instanceof ApiError
? err.message
: "Something went wrong while booking your ride. Please try again.";
Alert.alert("Error", msg);
: t("confirmRide.alertErrorFallback");
Alert.alert(t("confirmRide.alertErrorTitle"), msg);
} finally {
setProcessing(false);
}
@@ -203,11 +211,11 @@ const ConfirmRide = () => {
if (method === "card") {
Alert.alert(
"Pay by card",
`Your card will be charged $${estimate.fare}.`,
t("confirmRide.alertPayCardTitle"),
t("confirmRide.alertPayCardBody", { fare: estimate.fare }),
[
{ text: "Cancel", style: "cancel" },
{ text: "Continue", onPress: () => void doRequest() },
{ text: t("common.cancel"), style: "cancel" },
{ text: t("common.continue"), onPress: () => void doRequest() },
],
);
} else {
@@ -216,39 +224,53 @@ const ConfirmRide = () => {
};
return (
<RideLayout title="Request Ride" snapPoints={["60%", "88%"]}>
<Text className="text-xl font-JakartaSemiBold mb-1">Your trip</Text>
<RideLayout title={t("confirmRide.title")} snapPoints={["60%", "88%"]}>
<Text className="text-xl font-JakartaSemiBold mb-1 text-black dark:text-white">
{t("confirmRide.yourTrip")}
</Text>
<View className="flex-row items-center gap-x-2 mb-1">
<Text className="text-general-200 text-xs">Pickup</Text>
<Text className="text-general-200 dark:text-neutral-400 text-xs">
{t("confirmRide.pickup")}
</Text>
</View>
<Text className="font-JakartaMedium mb-3" numberOfLines={1}>
<Text className="font-JakartaMedium mb-3 text-black dark:text-white" numberOfLines={1}>
{userAddress}
</Text>
<View className="flex-row items-center gap-x-2 mb-1">
<Text className="text-general-200 text-xs">Destination</Text>
<Text className="text-general-200 dark:text-neutral-400 text-xs">
{t("confirmRide.destination")}
</Text>
</View>
<Text className="font-JakartaMedium mb-4" numberOfLines={1}>
<Text className="font-JakartaMedium mb-4 text-black dark:text-white" numberOfLines={1}>
{destinationAddress}
</Text>
<View className="flex-row items-center justify-between bg-general-500 rounded-2xl p-4 mb-4">
<View className="flex-row items-center justify-between bg-general-500 dark:bg-neutral-950 rounded-2xl p-4 mb-4">
<View>
<Text className="text-general-200 text-xs font-JakartaMedium">
{selected.label} · {selected.tagline}
<Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
{t(selected.labelKey)} · {t(selected.taglineKey)}
</Text>
<Text className="text-general-200 text-xs mt-1">
Trip time {estimate ? formatTime(estimate.durationSeconds / 60) : "…"}
<Text className="text-general-200 dark:text-neutral-400 text-xs mt-1">
{t("confirmRide.tripTime", {
time: estimate ? formatTime(estimate.durationSeconds / 60) : "…",
})}
</Text>
</View>
<View className="items-end">
<Text className="text-2xl font-JakartaExtraBold">
{estimating ? "…" : estimate ? `$${estimate.fare}` : "—"}
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
{estimating
? "…"
: estimate
? t("confirmRide.fareDisplay", { fare: estimate.fare })
: "—"}
</Text>
{estimate ? (
<Text className="text-xs text-general-200">
{formatLBP(parseFloat(estimate.fare))}
<Text className="text-xs text-general-200 dark:text-neutral-400">
{t("confirmRide.lbpEstimate", {
lbp: formatLBP(parseFloat(estimate.fare)),
})}
</Text>
) : null}
</View>
@@ -256,50 +278,52 @@ const ConfirmRide = () => {
<Text
className={`text-base font-JakartaMedium mb-2 ${
driversOnline === 0 ? "text-rose-500" : "text-general-200"
driversOnline === 0
? "text-rose-500"
: "text-general-200 dark:text-neutral-400"
}`}
>
{driversOnline === 0
? `No ${selected.label} drivers online right now`
? t("confirmRide.noDrivers", { service: t(selected.labelKey) })
: nearestEta == null
? "Finding drivers nearby…"
: `Nearest driver${nearestEta} min away`}
? t("confirmRide.findingDrivers")
: t("confirmRide.nearestDriver", { eta: nearestEta })}
</Text>
<Text className="text-lg font-JakartaSemiBold mt-2 mb-2">
Payment Method
<Text className="text-lg font-JakartaSemiBold mt-2 mb-2 text-black dark:text-white">
{t("confirmRide.paymentMethod")}
</Text>
<View className="flex-row gap-x-3 mb-2">
<TouchableOpacity
onPress={() => setMethod("cash")}
className={`flex-1 items-center py-3 rounded-xl border ${
method === "cash"
? "bg-general-600 border-primary-500"
: "bg-white border-general-700"
? "bg-general-600 dark:bg-primary-500/20 border-primary-500"
: "bg-white dark:bg-neutral-900 border-general-700 dark:border-neutral-700"
}`}
>
<Text
className={`font-JakartaMedium ${
method === "cash" ? "text-white" : "text-black"
method === "cash" ? "text-white" : "text-black dark:text-white"
}`}
>
💵 Cash
{t("confirmRide.cash")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setMethod("card")}
className={`flex-1 items-center py-3 rounded-xl border ${
method === "card"
? "bg-general-600 border-primary-500"
: "bg-white border-general-700"
? "bg-general-600 dark:bg-primary-500/20 border-primary-500"
: "bg-white dark:bg-neutral-900 border-general-700 dark:border-neutral-700"
}`}
>
<Text
className={`font-JakartaMedium ${
method === "card" ? "text-white" : "text-black"
method === "card" ? "text-white" : "text-black dark:text-white"
}`}
>
💳 Card
{t("confirmRide.card")}
</Text>
</TouchableOpacity>
</View>
@@ -307,12 +331,12 @@ const ConfirmRide = () => {
<CustomButton
title={
processing
? "Requesting"
? t("confirmRide.requesting")
: driversOnline === 0
? "No drivers online"
? t("confirmRide.noDriversOnline")
: method === "cash"
? "Request Ride · Pay cash to driver"
: "Request Ride · Pay by card"
? t("confirmRide.requestCash")
: t("confirmRide.requestCard")
}
className="mt-4"
onPress={request}
+147 -115
View File
@@ -17,7 +17,9 @@ import { CustomButton } from "@/components/custom-button";
import { icons, images } from "@/constants";
import { SERVICES, type ServiceId } from "@/constants/services";
import { ApiError, fetchAPI } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import { useSession } from "@/lib/session";
import { useTheme } from "@/lib/theme";
import { useDriverLocation } from "@/lib/use-driver-location";
import { formatTime } from "@/lib/utils";
@@ -71,6 +73,8 @@ type Dashboard = {
const DriverHome = () => {
const { signOut, user } = useSession();
const { isDark } = useTheme();
const t = useT();
const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState<Profile | null>(null);
const [online, setOnline] = useState(false);
@@ -136,7 +140,7 @@ const DriverHome = () => {
if (!next) setDashboard(null);
} catch (err) {
console.log("[DRIVER_TOGGLE]: ", err);
Alert.alert("Error", "Could not change your status. Please try again.");
Alert.alert(t("driver.home.alertErrorTitle"), t("driver.home.alertToggleBody"));
} finally {
setBusy(false);
}
@@ -154,10 +158,10 @@ const DriverHome = () => {
} catch (err) {
console.log("[DRIVER_RESPOND]: ", err);
Alert.alert(
"Error",
t("driver.activeRide.alertErrorTitle"),
action === "accept"
? "Could not accept this ride. It may have been taken or expired."
: "Could not decline this ride. Please try again.",
? t("driver.activeRide.alertAcceptBody")
: t("driver.activeRide.alertDeclineBody"),
);
} finally {
setBusy(false);
@@ -175,7 +179,7 @@ const DriverHome = () => {
await fetchDashboard();
} catch (err) {
console.log("[DRIVER_ADVANCE]: ", err);
Alert.alert("Error", "Could not update the ride. Please try again.");
Alert.alert(t("driver.activeRide.alertErrorTitle"), t("driver.activeRide.alertUpdateBody"));
} finally {
setBusy(false);
}
@@ -183,8 +187,8 @@ const DriverHome = () => {
if (loading) {
return (
<SafeAreaView className="flex-1 bg-white items-center justify-center">
<ActivityIndicator size="large" color="#0286ff" />
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 items-center justify-center">
<ActivityIndicator size="large" color={isDark ? "#0286ff" : "#0286ff"} />
</SafeAreaView>
);
}
@@ -199,20 +203,20 @@ const DriverHome = () => {
const rideCount = dashboard?.recent.length ?? 0;
return (
<SafeAreaView className="flex-1 bg-general-500">
<SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
<ScrollView
className="flex-1 px-5"
contentContainerStyle={{ paddingBottom: 40 }}
>
<View className="flex-row items-center justify-between my-5">
<Text className="text-2xl font-JakartaExtraBold">
Driver mode
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
{t("driver.home.driverMode")}
</Text>
<TouchableOpacity
onPress={signOut}
className="w-10 h-10 rounded-full bg-white items-center justify-center"
className="w-10 h-10 rounded-full bg-white dark:bg-neutral-900 items-center justify-center"
>
<Image source={icons.out} className="w-4 h-4" alt="Sign out" />
<Image source={icons.out} className="w-4 h-4" alt={t("driver.home.signOutAlt")} />
</TouchableOpacity>
</View>
@@ -221,29 +225,29 @@ const DriverHome = () => {
onPress={toggleOnline}
disabled={busy}
className={`rounded-2xl p-5 items-center mb-4 ${
online ? "bg-emerald-500" : "bg-neutral-700"
online ? "bg-emerald-500" : "bg-neutral-700 dark:bg-neutral-800"
}`}
>
<Text className="text-white text-lg font-JakartaBold">
{online ? "● Online — receiving ride requests" : "○ Go online to drive"}
{online ? t("driver.home.online") : t("driver.home.goOnline")}
</Text>
</TouchableOpacity>
{/* Earnings summary */}
<View className="bg-white rounded-2xl p-4 mb-4 flex-row justify-between">
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mb-4 flex-row justify-between">
<View>
<Text className="text-general-200 text-xs font-JakartaMedium">
Today&apos;s earnings
<Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
{t("driver.home.todaysEarnings")}
</Text>
<Text className="text-2xl font-JakartaExtraBold">
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
${(earnings / 100).toFixed(2)}
</Text>
</View>
<View className="items-end">
<Text className="text-general-200 text-xs font-JakartaMedium">
Completed today
<Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
{t("driver.home.completedToday")}
</Text>
<Text className="text-2xl font-JakartaExtraBold">{rideCount}</Text>
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">{rideCount}</Text>
</View>
</View>
@@ -257,8 +261,10 @@ const DriverHome = () => {
) : null}
{/* Incoming offers */}
<Text className="text-xl font-JakartaBold mt-4 mb-3">
Incoming requests {online ? "" : "(offline)"}
<Text className="text-xl font-JakartaBold mt-4 mb-3 text-black dark:text-white">
{online
? t("driver.home.incomingRequests")
: t("driver.home.incomingRequestsOffline")}
</Text>
{!online ? null : dashboard?.offers.length ? (
@@ -272,10 +278,10 @@ const DriverHome = () => {
/>
))
) : (
<View className="bg-white rounded-2xl p-6 items-center">
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-6 items-center">
<Image source={images.noResult} className="w-24 h-24" resizeMode="contain" />
<Text className="text-general-200 mt-2">
{online ? "Waiting for ride requests" : "Go online to start driving."}
<Text className="text-general-200 dark:text-neutral-400 mt-2">
{online ? t("driver.home.waitingRequests") : t("driver.home.goOnlineStart")}
</Text>
</View>
)}
@@ -295,6 +301,8 @@ const Onboarding = ({
signOut: () => Promise<void>;
userName?: string | null;
}) => {
const t = useT();
const { isDark } = useTheme();
const [service, setService] = useState<ServiceId>("car");
const [carModel, setCarModel] = useState("");
const [carSeats, setCarSeats] = useState("4");
@@ -303,7 +311,7 @@ const Onboarding = ({
const submit = async () => {
const seats = Number(carSeats);
if (!Number.isInteger(seats) || seats < 1 || seats > 8) {
Alert.alert("Invalid seats", "Car seats must be a whole number 18.");
Alert.alert(t("driver.home.alertSeatsTitle"), t("driver.home.alertSeatsBody"));
return;
}
setSubmitting(true);
@@ -320,33 +328,35 @@ const Onboarding = ({
await onCreated();
} catch (err) {
console.log("[DRIVER_ONBOARD]: ", err);
Alert.alert("Error", "Could not create your driver profile. Please try again.");
Alert.alert(t("driver.home.alertErrorTitle"), t("driver.home.alertCreateBody"));
} finally {
setSubmitting(false);
}
};
return (
<SafeAreaView className="flex-1 bg-white">
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950">
<ScrollView className="flex-1 px-5" contentContainerStyle={{ paddingBottom: 40 }}>
<View className="flex-row items-center justify-between my-5">
<Text className="text-2xl font-JakartaExtraBold">
Welcome, {userName?.split(" ")[0] || "driver"}
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
{t("driver.home.welcome", {
name: userName?.split(" ")[0] || t("driver.home.welcomeFallback"),
})}
</Text>
<TouchableOpacity
onPress={signOut}
className="w-10 h-10 rounded-full bg-neutral-100 items-center justify-center"
className="w-10 h-10 rounded-full bg-neutral-100 dark:bg-neutral-900 items-center justify-center"
>
<Image source={icons.out} className="w-4 h-4" alt="Sign out" />
<Image source={icons.out} className="w-4 h-4" alt={t("driver.home.signOutAlt")} />
</TouchableOpacity>
</View>
<Text className="text-base text-general-200 font-Jakarta mb-4">
Set up your driver profile to start receiving ride requests.
<Text className="text-base text-general-200 dark:text-neutral-400 font-Jakarta mb-4">
{t("driver.home.setupIntro")}
</Text>
<Text className="text-lg font-JakartaSemiBold mb-3">
What will you drive?
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
{t("driver.home.whatDrive")}
</Text>
<View className="flex-row gap-2 mb-5">
{SERVICES.map((item) => {
@@ -358,46 +368,52 @@ const Onboarding = ({
className={`flex-1 items-center rounded-2xl border py-3 ${
active
? "border-primary-500 bg-primary-500/10"
: "border-neutral-100 bg-neutral-100"
: "border-neutral-100 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900"
}`}
>
<MaterialCommunityIcons
name={item.icon}
size={24}
color={active ? "#0286ff" : "#858585"}
color={active ? "#0286ff" : isDark ? "#9ca3af" : "#858585"}
/>
<Text
className={`mt-1.5 text-xs font-JakartaBold ${
active ? "text-primary-500" : "text-black"
active ? "text-primary-500" : "text-black dark:text-white"
}`}
>
{item.label}
{t(item.labelKey)}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text className="text-lg font-JakartaSemiBold mb-3">Car model</Text>
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
{t("driver.home.carModel")}
</Text>
<TextInput
value={carModel}
onChangeText={setCarModel}
placeholder="e.g. Toyota Camry"
className="bg-neutral-100 rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-4"
placeholder={t("driver.home.carModelPlaceholder")}
placeholderTextColor={isDark ? "#737373" : "#858585"}
className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-4"
autoCapitalize="words"
/>
<Text className="text-lg font-JakartaSemiBold mb-3">Car seats</Text>
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
{t("driver.home.carSeats")}
</Text>
<TextInput
value={carSeats}
onChangeText={setCarSeats}
placeholder="4"
placeholder={t("driver.home.carSeatsPlaceholder")}
placeholderTextColor={isDark ? "#737373" : "#858585"}
keyboardType="number-pad"
className="bg-neutral-100 rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-8"
className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-8"
/>
<CustomButton
title={submitting ? "Saving" : "Start driving"}
title={submitting ? t("common.saving") : t("driver.home.startDriving")}
onPress={submit}
disabled={submitting}
/>
@@ -418,63 +434,74 @@ const OfferCard = ({
busy: boolean;
onAccept: () => void;
onDecline: () => void;
}) => (
<View className="bg-white rounded-2xl p-4 mb-3">
<View className="flex-row items-center justify-between mb-2">
<Text className="text-sm font-JakartaBold text-primary-500">
New request · {offer.service}
</Text>
<Text className="text-xs text-general-200">
{offer.payment_status === "cash" ? "💵 Cash" : "💳 Card"}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mb-1">
<Image source={icons.to} alt="From" className="w-4 h-4" />
<Text className="font-JakartaMedium" numberOfLines={1}>
{offer.origin_address}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mb-3">
<Image source={icons.point} alt="To" className="w-4 h-4" />
<Text className="font-JakartaMedium" numberOfLines={1}>
{offer.destination_address}
</Text>
</View>
<View className="flex-row justify-between mb-3">
<Text className="text-general-200 text-xs">Trip time</Text>
<Text className="font-JakartaMedium text-xs">
{formatTime(offer.ride_time)}
</Text>
</View>
<View className="flex-row justify-between mb-3">
<Text className="text-general-200 text-xs">Fare</Text>
<Text className="font-JakartaMedium text-xs text-emerald-600">
${(offer.fare_price / 100).toFixed(2)}
</Text>
</View>
<View className="flex-row gap-3">
<TouchableOpacity
onPress={onDecline}
disabled={busy}
className="flex-1 rounded-full py-3 bg-neutral-200 items-center"
>
<Text className="font-JakartaBold text-neutral-700">Decline</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onAccept}
disabled={busy}
className="flex-1 rounded-full py-3 bg-emerald-500 items-center"
>
<Text className="font-JakartaBold text-white">
{busy ? "…" : "Accept"}
}) => {
const t = useT();
return (
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mb-3">
<View className="flex-row items-center justify-between mb-2">
<Text className="text-sm font-JakartaBold text-primary-500">
{t("driver.offerCard.newRequest", { service: offer.service })}
</Text>
</TouchableOpacity>
<Text className="text-xs text-general-200 dark:text-neutral-400">
{offer.payment_status === "cash"
? t("driver.offerCard.cash")
: t("driver.offerCard.card")}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mb-1">
<Image source={icons.to} alt={t("driver.offerCard.fromAlt")} className="w-4 h-4" />
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
{offer.origin_address}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mb-3">
<Image source={icons.point} alt={t("driver.offerCard.toAlt")} className="w-4 h-4" />
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
{offer.destination_address}
</Text>
</View>
<View className="flex-row justify-between mb-3">
<Text className="text-general-200 dark:text-neutral-400 text-xs">
{t("driver.offerCard.tripTime")}
</Text>
<Text className="font-JakartaMedium text-xs text-black dark:text-white">
{formatTime(offer.ride_time)}
</Text>
</View>
<View className="flex-row justify-between mb-3">
<Text className="text-general-200 dark:text-neutral-400 text-xs">
{t("driver.offerCard.fare")}
</Text>
<Text className="font-JakartaMedium text-xs text-emerald-600 dark:text-emerald-400">
${(offer.fare_price / 100).toFixed(2)}
</Text>
</View>
<View className="flex-row gap-3">
<TouchableOpacity
onPress={onDecline}
disabled={busy}
className="flex-1 rounded-full py-3 bg-neutral-200 dark:bg-neutral-800 items-center"
>
<Text className="font-JakartaBold text-neutral-700 dark:text-neutral-200">
{t("driver.offerCard.decline")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onAccept}
disabled={busy}
className="flex-1 rounded-full py-3 bg-emerald-500 items-center"
>
<Text className="font-JakartaBold text-white">
{busy ? "…" : t("driver.offerCard.accept")}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
);
};
// --- Active ride card -----------------------------------------------------
@@ -487,11 +514,12 @@ const ActiveRideCard = ({
busy: boolean;
onAdvance: (rideId: number, status: "en_route" | "completed") => void;
}) => {
const t = useT();
const statusLabel =
ride.status === "accepted"
? "Head to pickup"
? t("driver.activeRide.headToPickup")
: ride.status === "en_route"
? "Trip in progress"
? t("driver.activeRide.tripInProgress")
: ride.status;
return (
@@ -500,36 +528,40 @@ const ActiveRideCard = ({
<Text className="text-sm font-JakartaBold text-primary-500">
{statusLabel}
</Text>
<Text className="text-xs text-general-200">{ride.service}</Text>
<Text className="text-xs text-general-200 dark:text-neutral-400">{ride.service}</Text>
</View>
{ride.rider_name ? (
<Text className="font-JakartaBold mb-2">{ride.rider_name}</Text>
<Text className="font-JakartaBold mb-2 text-black dark:text-white">
{t("driver.activeRide.rider", { name: ride.rider_name })}
</Text>
) : null}
<View className="flex-row items-center gap-x-2 mb-1">
<Image source={icons.to} alt="From" className="w-4 h-4" />
<Text className="font-JakartaMedium" numberOfLines={1}>
<Image source={icons.to} alt={t("driver.activeRide.fromAlt")} className="w-4 h-4" />
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
{ride.origin_address}
</Text>
</View>
<View className="flex-row items-center gap-x-2 mb-3">
<Image source={icons.point} alt="To" className="w-4 h-4" />
<Text className="font-JakartaMedium" numberOfLines={1}>
<Image source={icons.point} alt={t("driver.activeRide.toAlt")} className="w-4 h-4" />
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
{ride.destination_address}
</Text>
</View>
<View className="flex-row justify-between mb-4">
<Text className="text-general-200 text-xs">Fare</Text>
<Text className="font-JakartaMedium text-xs text-emerald-600">
<Text className="text-general-200 dark:text-neutral-400 text-xs">
{t("driver.activeRide.fare")}
</Text>
<Text className="font-JakartaMedium text-xs text-emerald-600 dark:text-emerald-400">
${(ride.fare_price / 100).toFixed(2)}
</Text>
</View>
{ride.status === "accepted" ? (
<CustomButton
title={busy ? "…" : "Start trip"}
title={busy ? "…" : t("driver.activeRide.startTrip")}
bgVariant="success"
onPress={() => onAdvance(ride.ride_id, "en_route")}
className="mb-2"
@@ -537,7 +569,7 @@ const ActiveRideCard = ({
) : null}
{ride.status === "en_route" ? (
<CustomButton
title={busy ? "…" : "Complete trip"}
title={busy ? "…" : t("driver.activeRide.completeTrip")}
bgVariant="success"
onPress={() => onAdvance(ride.ride_id, "completed")}
/>
+13 -9
View File
@@ -2,11 +2,13 @@ import { CustomButton } from "@/components/custom-button";
import { GoogleTextInput } from "@/components/google-text-input";
import { RideLayout } from "@/components/ride-layout";
import { icons } from "@/constants";
import { useT } from "@/lib/i18n";
import { useLocationStore } from "@/store";
import { router } from "expo-router";
import { Text, View } from "react-native";
const FindRide = () => {
const t = useT();
const {
userAddress,
destinationAddress,
@@ -25,33 +27,35 @@ const FindRide = () => {
!!destinationLongitude;
return (
<RideLayout title="Ride" snapPoints={["85%"]}>
<RideLayout title={t("findRide.title")} snapPoints={["85%"]}>
<View className="my-3">
<Text className="text-lg font-JakartaSemiBold mb-3">From</Text>
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
{t("findRide.from")}
</Text>
<GoogleTextInput
icon={icons.target}
initialLocation={userAddress ?? ""}
containerStyles="bg-neutral-100"
textInputBackgroundColor="#F5F5F5"
containerStyles="bg-neutral-100 dark:bg-neutral-800"
handlePress={setUserLocation}
/>
</View>
<View className="my-3">
<Text className="text-lg font-JakartaSemiBold mb-3">To</Text>
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
{t("findRide.to")}
</Text>
<GoogleTextInput
icon={icons.map}
initialLocation={destinationAddress ?? ""}
containerStyles="bg-neutral-100"
textInputBackgroundColor="transparent"
containerStyles="bg-neutral-100 dark:bg-neutral-800"
handlePress={setDestinationLocation}
/>
</View>
<CustomButton
title="Find now"
title={t("findRide.findNow")}
onPress={() => router.push("/(root)/confirm-ride")}
disabled={!canFind}
className={`mt-5 ${!canFind ? "opacity-50" : ""}`}
@@ -60,4 +64,4 @@ const FindRide = () => {
);
};
export default FindRide;
export default FindRide;
+17 -13
View File
@@ -4,10 +4,12 @@ import { Alert, Text, TouchableOpacity, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { fetchAPI } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import { useSession } from "@/lib/session";
const RoleSelection = () => {
const { setUserRole } = useSession();
const t = useT();
const [saving, setSaving] = useState(false);
const chooseRole = async (role: "rider" | "driver") => {
@@ -31,20 +33,20 @@ const RoleSelection = () => {
);
} catch (err) {
console.log("[ROLE_SELECT]: ", err);
Alert.alert("Error", "Could not save your choice. Please try again.");
Alert.alert(t("auth.role.alertErrorTitle"), t("auth.role.alertErrorBody"));
} finally {
setSaving(false);
}
};
return (
<SafeAreaView className="flex-1 bg-white justify-center px-7">
<Text className="text-3xl font-JakartaExtraBold text-center">
How will you use Waseel?
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 justify-center px-7">
<Text className="text-3xl font-JakartaExtraBold text-center text-black dark:text-white">
{t("auth.role.title")}
</Text>
<Text className="text-base text-general-200 font-Jakarta text-center mt-3 mb-10">
You can change this later by contacting support.
<Text className="text-base text-general-200 dark:text-neutral-400 font-Jakarta text-center mt-3 mb-10">
{t("auth.role.subtitle")}
</Text>
<TouchableOpacity
@@ -54,26 +56,28 @@ const RoleSelection = () => {
>
<Text className="text-5xl mb-3">🧍</Text>
<Text className="text-2xl font-JakartaBold text-white">
I&apos;m a Rider
{t("auth.role.riderTitle")}
</Text>
<Text className="text-sm font-Jakarta text-white/80 text-center mt-2">
Book rides and get around Lebanon
{t("auth.role.riderDesc")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => chooseRole("driver")}
disabled={saving}
className="bg-general-600 rounded-2xl p-7 items-center"
className="bg-general-600 dark:bg-primary-500/20 border border-primary-500 rounded-2xl p-7 items-center"
>
<Text className="text-5xl mb-3">🚗</Text>
<Text className="text-2xl font-JakartaBold">I&apos;m a Driver</Text>
<Text className="text-sm font-Jakarta text-general-200 text-center mt-2">
Give rides and earn money
<Text className="text-2xl font-JakartaBold text-black dark:text-white">
{t("auth.role.driverTitle")}
</Text>
<Text className="text-sm font-Jakarta text-general-200 dark:text-neutral-400 text-center mt-2">
{t("auth.role.driverDesc")}
</Text>
</TouchableOpacity>
</SafeAreaView>
);
};
export default RoleSelection;
export default RoleSelection;