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;