Waseel: driver app, dispatch, POI suggestions, map fixes
This commit is contained in:
+51
-49
@@ -18,11 +18,13 @@ import { OAuth } from "@/components/oauth";
|
||||
import { OtpField } from "@/components/otp-field";
|
||||
import { icons, images } from "@/constants";
|
||||
import { ApiError, fetchAPI } from "@/lib/fetch";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { getRememberedEmail, rememberEmail, useSession } from "@/lib/session";
|
||||
|
||||
const SignIn = () => {
|
||||
const router = useRouter();
|
||||
const { setSession } = useSession();
|
||||
const t = useT();
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
@@ -84,7 +86,7 @@ const SignIn = () => {
|
||||
|
||||
const onRequestReset = async () => {
|
||||
if (!reset.email.trim()) {
|
||||
setReset((prev) => ({ ...prev, error: "Enter your email address." }));
|
||||
setReset((prev) => ({ ...prev, error: t("auth.signIn.errEmail") }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,14 +114,14 @@ const SignIn = () => {
|
||||
|
||||
const onSubmitReset = async () => {
|
||||
if (!/^\d{6}$/.test(reset.code)) {
|
||||
setReset((prev) => ({ ...prev, error: "Enter the 6-digit code." }));
|
||||
setReset((prev) => ({ ...prev, error: t("auth.signIn.errCode") }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (reset.password.length < 8) {
|
||||
setReset((prev) => ({
|
||||
...prev,
|
||||
error: "Password must be at least 8 characters.",
|
||||
error: t("auth.signIn.errPassword"),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
@@ -148,7 +150,7 @@ const SignIn = () => {
|
||||
error:
|
||||
err instanceof ApiError && err.status < 500
|
||||
? err.message
|
||||
: "Could not reset your password. Please try again.",
|
||||
: t("auth.signIn.errReset"),
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -160,8 +162,8 @@ const SignIn = () => {
|
||||
// otherwise surfaces as a generic "could not sign in".
|
||||
if (!form.email.trim() || !form.password) {
|
||||
Alert.alert(
|
||||
"Missing information",
|
||||
"Enter both your email and your password.",
|
||||
t("auth.signIn.alertMissingTitle"),
|
||||
t("auth.signIn.alertMissingBody"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -186,9 +188,9 @@ const SignIn = () => {
|
||||
const message =
|
||||
err instanceof ApiError && err.status < 500
|
||||
? err.message
|
||||
: "Could not sign in. Please try again.";
|
||||
: t("auth.signIn.alertErrorFallback");
|
||||
|
||||
Alert.alert("Error", message);
|
||||
Alert.alert(t("auth.signIn.alertErrorTitle"), message);
|
||||
|
||||
// Only a rejected password is worth retyping. Clearing it after a
|
||||
// network blip or a 403 just makes the next attempt fail differently.
|
||||
@@ -198,38 +200,38 @@ const SignIn = () => {
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [busy, form.email, form.password, remember, setSession, router]);
|
||||
}, [busy, form.email, form.password, remember, setSession, router, t]);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-neutral-950"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 40 : 0}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-neutral-950"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1 bg-white dark:bg-neutral-950">
|
||||
<View className="relative w-full h-[250px]">
|
||||
<Image
|
||||
source={images.signUpCar}
|
||||
alt="Car"
|
||||
alt={t("auth.signIn.carAlt")}
|
||||
className="z-0 w-full h-[250px]"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
<Text className="text-2xl text-black font-JakartaSemiBold absolute bottom-5 left-5">
|
||||
Welcome 👋
|
||||
<Text className="text-2xl text-black dark:text-white font-JakartaSemiBold absolute bottom-5 left-5">
|
||||
{t("auth.signIn.welcome")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="p-5">
|
||||
<InputField
|
||||
label="Email"
|
||||
placeholder="karim@email.com"
|
||||
label={t("auth.signIn.email")}
|
||||
placeholder={t("auth.signIn.emailPlaceholder")}
|
||||
icon={icons.email}
|
||||
value={form.email}
|
||||
onChangeText={(value) =>
|
||||
@@ -244,8 +246,8 @@ const SignIn = () => {
|
||||
/>
|
||||
|
||||
<InputField
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
label={t("auth.signIn.password")}
|
||||
placeholder={t("auth.signIn.passwordPlaceholder")}
|
||||
icon={icons.lock}
|
||||
secureTextEntry
|
||||
value={form.password}
|
||||
@@ -270,7 +272,7 @@ const SignIn = () => {
|
||||
className={`h-6 w-6 rounded-md items-center justify-center border-2 ${
|
||||
remember
|
||||
? "bg-primary-500 border-primary-500"
|
||||
: "bg-white border-neutral-300"
|
||||
: "bg-white dark:bg-neutral-900 border-neutral-300 dark:border-neutral-700"
|
||||
}`}
|
||||
>
|
||||
{remember ? (
|
||||
@@ -278,13 +280,13 @@ const SignIn = () => {
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Text className="ml-3 font-JakartaMedium text-[15px] text-black">
|
||||
Keep me signed in
|
||||
<Text className="ml-3 font-JakartaMedium text-[15px] text-black dark:text-white">
|
||||
{t("auth.signIn.keepSignedIn")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<CustomButton
|
||||
title={busy ? "Signing in…" : "Sign In"}
|
||||
title={busy ? t("auth.signIn.signingIn") : t("auth.signIn.signInBtn")}
|
||||
onPress={onSignInPress}
|
||||
disabled={busy}
|
||||
className="mt-6"
|
||||
@@ -292,18 +294,18 @@ const SignIn = () => {
|
||||
|
||||
<TouchableOpacity onPress={openReset} className="mt-4">
|
||||
<Text className="text-primary-500 text-center font-JakartaMedium">
|
||||
Forgot password?
|
||||
{t("auth.signIn.forgotPassword")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<OAuth title="Sign in with Google" />
|
||||
<OAuth title={t("auth.signIn.signInGoogle")} />
|
||||
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="text-base text-center text-general-200 mt-10"
|
||||
className="text-base text-center text-general-200 dark:text-neutral-400 mt-10"
|
||||
>
|
||||
<Text>Don't have an account? </Text>
|
||||
<Text className="text-primary-500">Sign up</Text>
|
||||
<Text className="text-black dark:text-white">{t("auth.signIn.noAccount")}</Text>
|
||||
<Text className="text-primary-500">{t("auth.signIn.signUpLink")}</Text>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
@@ -311,18 +313,18 @@ const SignIn = () => {
|
||||
isVisible={reset.state === "request"}
|
||||
onBackdropPress={closeReset}
|
||||
>
|
||||
<View className="bg-white px-7 py-9 rounded-2xl min-h-[280px]">
|
||||
<Text className="text-2xl font-JakartaExtraBold mb-2">
|
||||
Reset password
|
||||
<View className="bg-white dark:bg-neutral-900 px-7 py-9 rounded-2xl min-h-[280px]">
|
||||
<Text className="text-2xl font-JakartaExtraBold mb-2 text-black dark:text-white">
|
||||
{t("auth.signIn.reset.title")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-Jakarta mb-5">
|
||||
Enter your email and we'll send you a 6-digit reset code.
|
||||
<Text className="font-Jakarta mb-5 text-black dark:text-white">
|
||||
{t("auth.signIn.reset.requestBody")}
|
||||
</Text>
|
||||
|
||||
<InputField
|
||||
label="Email"
|
||||
placeholder="karim@email.com"
|
||||
label={t("auth.signIn.email")}
|
||||
placeholder={t("auth.signIn.reset.emailPlaceholder")}
|
||||
icon={icons.email}
|
||||
value={reset.email}
|
||||
keyboardType="email-address"
|
||||
@@ -338,7 +340,7 @@ const SignIn = () => {
|
||||
) : null}
|
||||
|
||||
<CustomButton
|
||||
title={reset.busy ? "Sending…" : "Send Code"}
|
||||
title={reset.busy ? t("auth.signIn.reset.sending") : t("auth.signIn.reset.sendCode")}
|
||||
onPress={onRequestReset}
|
||||
disabled={reset.busy}
|
||||
className="mt-5"
|
||||
@@ -350,20 +352,20 @@ const SignIn = () => {
|
||||
isVisible={reset.state === "reset"}
|
||||
onBackdropPress={closeReset}
|
||||
>
|
||||
<View className="bg-white px-7 py-9 rounded-2xl min-h-[300px]">
|
||||
<Text className="text-2xl font-JakartaExtraBold mb-2">
|
||||
Enter new password
|
||||
<View className="bg-white dark:bg-neutral-900 px-7 py-9 rounded-2xl min-h-[300px]">
|
||||
<Text className="text-2xl font-JakartaExtraBold mb-2 text-black dark:text-white">
|
||||
{t("auth.signIn.reset.newPassTitle")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-Jakarta mb-5">
|
||||
We've sent a reset code to {reset.email}
|
||||
<Text className="font-Jakarta mb-5 text-black dark:text-white">
|
||||
{t("auth.signIn.reset.resetBody", { email: reset.email })}
|
||||
</Text>
|
||||
|
||||
{reset.devCode ? (
|
||||
<View className="bg-amber-50 border border-amber-300 rounded-xl p-3 mb-5">
|
||||
<Text className="text-sm text-amber-700 font-Jakarta">
|
||||
Email delivery is not configured on this server. Your reset
|
||||
code is <Text className="font-JakartaBold">{reset.devCode}</Text>
|
||||
<View className="bg-amber-50 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-800 rounded-xl p-3 mb-5">
|
||||
<Text className="text-sm text-amber-700 dark:text-amber-400 font-Jakarta">
|
||||
{t("auth.signIn.reset.devBanner")}
|
||||
<Text className="font-JakartaBold">{reset.devCode}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -376,9 +378,9 @@ const SignIn = () => {
|
||||
/>
|
||||
|
||||
<InputField
|
||||
label="New password"
|
||||
label={t("auth.signIn.reset.newPassLabel")}
|
||||
icon={icons.lock}
|
||||
placeholder="••••••••"
|
||||
placeholder={t("auth.signIn.reset.newPasswordPlaceholder")}
|
||||
secureTextEntry
|
||||
value={reset.password}
|
||||
onChangeText={(password) =>
|
||||
@@ -393,7 +395,7 @@ const SignIn = () => {
|
||||
) : null}
|
||||
|
||||
<CustomButton
|
||||
title={reset.busy ? "Resetting…" : "Reset Password"}
|
||||
title={reset.busy ? t("auth.signIn.reset.resetting") : t("auth.signIn.reset.resetBtn")}
|
||||
onPress={onSubmitReset}
|
||||
disabled={reset.busy}
|
||||
className="mt-5 bg-emerald-500"
|
||||
@@ -406,4 +408,4 @@ const SignIn = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
export default SignIn;
|
||||
+61
-58
@@ -18,18 +18,21 @@ import { OAuth } from "@/components/oauth";
|
||||
import { OtpField } from "@/components/otp-field";
|
||||
import { icons, images } from "@/constants";
|
||||
import { ApiError, fetchAPI } from "@/lib/fetch";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useSession } from "@/lib/session";
|
||||
|
||||
const ROLES = [
|
||||
{
|
||||
value: "rider",
|
||||
title: "I need a ride",
|
||||
description: "Book rides and get where you're going",
|
||||
titleKey: "auth.signUp.riderTitle",
|
||||
descKey: "auth.signUp.riderDesc",
|
||||
icon: "map" as const,
|
||||
},
|
||||
{
|
||||
value: "driver",
|
||||
title: "I want to drive",
|
||||
description: "Offer rides and earn money with your car",
|
||||
titleKey: "auth.signUp.driverTitle",
|
||||
descKey: "auth.signUp.driverDesc",
|
||||
icon: "dollar" as const,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -37,6 +40,7 @@ type Role = (typeof ROLES)[number]["value"];
|
||||
|
||||
const SignUp = () => {
|
||||
const { setSession } = useSession();
|
||||
const t = useT();
|
||||
|
||||
const [role, setRole] = useState<Role>("rider");
|
||||
const [form, setForm] = useState({
|
||||
@@ -59,16 +63,16 @@ const SignUp = () => {
|
||||
const onSignUpPress = async () => {
|
||||
if (!form.name.trim() || !form.email.trim() || !form.password) {
|
||||
Alert.alert(
|
||||
"Missing information",
|
||||
"Please fill in your name, email and password.",
|
||||
t("auth.signUp.alertMissingTitle"),
|
||||
t("auth.signUp.alertMissingBody"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.phone.trim() && !/^[0-9\s\-()+.]+$/.test(form.phone)) {
|
||||
Alert.alert(
|
||||
"Invalid phone number",
|
||||
"Enter a valid Lebanese number, e.g. 70 123 456.",
|
||||
t("auth.signUp.alertPhoneTitle"),
|
||||
t("auth.signUp.alertPhoneBody"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -105,10 +109,10 @@ const SignUp = () => {
|
||||
password: "",
|
||||
}));
|
||||
Alert.alert(
|
||||
"Error",
|
||||
t("auth.signUp.alertErrorTitle"),
|
||||
err instanceof ApiError && err.status < 500
|
||||
? err.message
|
||||
: "Could not create your account.",
|
||||
: t("auth.signUp.alertErrorFallback"),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -118,7 +122,7 @@ const SignUp = () => {
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
setVerification((prevVerification) => ({
|
||||
...prevVerification,
|
||||
error: "Enter the 6-digit code.",
|
||||
error: t("auth.signUp.errCode"),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
@@ -153,42 +157,42 @@ const SignUp = () => {
|
||||
error:
|
||||
err instanceof ApiError && err.status < 500
|
||||
? err.message
|
||||
: "Verification failed. Please try again.",
|
||||
: t("auth.signUp.errVerify"),
|
||||
}));
|
||||
}
|
||||
},
|
||||
[form.email, setSession],
|
||||
[form.email, setSession, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-neutral-950"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 40 : 0}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-neutral-950"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1 bg-white dark:bg-neutral-950">
|
||||
<View className="relative w-full h-[250px]">
|
||||
<Image
|
||||
source={images.signUpCar}
|
||||
alt="Car"
|
||||
alt={t("auth.signUp.carAlt")}
|
||||
className="z-0 w-full h-[250px]"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
<Text className="text-2xl text-black font-JakartaSemiBold absolute bottom-5 left-5">
|
||||
Create Your Account
|
||||
<Text className="text-2xl text-black dark:text-white font-JakartaSemiBold absolute bottom-5 left-5">
|
||||
{t("auth.signUp.createAccount")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="p-5">
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3">
|
||||
How will you use Waseel?
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
|
||||
{t("auth.signUp.howUse")}
|
||||
</Text>
|
||||
<View className="flex-row gap-3 mb-4">
|
||||
{ROLES.map((option) => {
|
||||
@@ -201,24 +205,24 @@ const SignUp = () => {
|
||||
className={`flex-1 justify-center rounded-2xl border p-4 ${
|
||||
selected
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
source={option.value === "driver" ? icons.dollar : icons.map}
|
||||
alt={`${option.title} icon`}
|
||||
alt={t(`auth.signUp.${option.value === "driver" ? "driverTitle" : "riderTitle"}`)}
|
||||
className="h-7 w-7 mb-2"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text
|
||||
className={`text-[15px] font-JakartaBold ${
|
||||
selected ? "text-primary-500" : "text-black"
|
||||
selected ? "text-primary-500" : "text-black dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{option.title}
|
||||
{t(option.titleKey)}
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-400 font-Jakarta mt-1">
|
||||
{option.description}
|
||||
<Text className="text-xs text-neutral-400 dark:text-neutral-500 font-Jakarta mt-1">
|
||||
{t(option.descKey)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -226,8 +230,8 @@ const SignUp = () => {
|
||||
</View>
|
||||
|
||||
<InputField
|
||||
label="Name"
|
||||
placeholder="Karim Haddad"
|
||||
label={t("auth.signUp.name")}
|
||||
placeholder={t("auth.signUp.namePlaceholder")}
|
||||
icon={icons.person}
|
||||
value={form.name}
|
||||
onChangeText={(value) =>
|
||||
@@ -240,8 +244,8 @@ const SignUp = () => {
|
||||
/>
|
||||
|
||||
<InputField
|
||||
label="Email"
|
||||
placeholder="karim@email.com"
|
||||
label={t("auth.signUp.email")}
|
||||
placeholder={t("auth.signUp.emailPlaceholder")}
|
||||
icon={icons.email}
|
||||
value={form.email}
|
||||
onChangeText={(value) =>
|
||||
@@ -254,8 +258,8 @@ const SignUp = () => {
|
||||
/>
|
||||
|
||||
<InputField
|
||||
label="Phone (optional)"
|
||||
placeholder="70 123 456"
|
||||
label={t("auth.signUp.phoneOptional")}
|
||||
placeholder={t("auth.signUp.phonePlaceholder")}
|
||||
icon={icons.chat}
|
||||
value={form.phone}
|
||||
onChangeText={(value) =>
|
||||
@@ -268,8 +272,8 @@ const SignUp = () => {
|
||||
/>
|
||||
|
||||
<InputField
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
label={t("auth.signUp.password")}
|
||||
placeholder={t("auth.signUp.passwordPlaceholder")}
|
||||
icon={icons.lock}
|
||||
secureTextEntry
|
||||
value={form.password}
|
||||
@@ -282,19 +286,19 @@ const SignUp = () => {
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
title="Sign Up"
|
||||
title={t("auth.signUp.signUpBtn")}
|
||||
onPress={onSignUpPress}
|
||||
className="mt-6"
|
||||
/>
|
||||
|
||||
<OAuth title="Sign up with Google" />
|
||||
<OAuth title={t("auth.signUp.signUpGoogle")} />
|
||||
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-base text-center text-general-200 mt-10"
|
||||
className="text-base text-center text-general-200 dark:text-neutral-400 mt-10"
|
||||
>
|
||||
<Text>Already have an account? </Text>
|
||||
<Text className="text-primary-500">Sign in</Text>
|
||||
<Text className="text-black dark:text-white">{t("auth.signUp.haveAccount")}</Text>
|
||||
<Text className="text-primary-500">{t("auth.signUp.signInLink")}</Text>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
@@ -308,20 +312,19 @@ const SignUp = () => {
|
||||
}
|
||||
isVisible={verification.state === "pending"}
|
||||
>
|
||||
<View className="bg-white px-7 py-9 rounded-2xl min-h-[300px]">
|
||||
<Text className="text-2xl font-JakartaExtraBold mb-2">
|
||||
Verification
|
||||
<View className="bg-white dark:bg-neutral-900 px-7 py-9 rounded-2xl min-h-[300px]">
|
||||
<Text className="text-2xl font-JakartaExtraBold mb-2 text-black dark:text-white">
|
||||
{t("auth.signUp.verify.title")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-Jakarta mb-5">
|
||||
We've sent a verification code to {form.email}
|
||||
<Text className="font-Jakarta mb-5 text-black dark:text-white">
|
||||
{t("auth.signUp.verify.body", { email: form.email })}
|
||||
</Text>
|
||||
|
||||
{verification.devCode ? (
|
||||
<View className="bg-amber-50 border border-amber-300 rounded-xl p-3 mb-5">
|
||||
<Text className="text-sm text-amber-700 font-Jakarta">
|
||||
Email delivery is not configured on this server. Your
|
||||
verification code is{" "}
|
||||
<View className="bg-amber-50 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-800 rounded-xl p-3 mb-5">
|
||||
<Text className="text-sm text-amber-700 dark:text-amber-400 font-Jakarta">
|
||||
{t("auth.signUp.verify.devBanner")}
|
||||
<Text className="font-JakartaBold">{verification.devCode}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -346,7 +349,7 @@ const SignUp = () => {
|
||||
) : null}
|
||||
|
||||
<CustomButton
|
||||
title={verification.busy ? "Verifying…" : "Verify Email"}
|
||||
title={verification.busy ? t("auth.signUp.verify.verifying") : t("auth.signUp.verify.verifyBtn")}
|
||||
onPress={() => onPressVerify(verification.code)}
|
||||
disabled={verification.busy}
|
||||
className="mt-5 bg-emerald-500"
|
||||
@@ -354,23 +357,23 @@ const SignUp = () => {
|
||||
</View>
|
||||
</ReactNativeModal>
|
||||
<ReactNativeModal isVisible={verification.state === "success"}>
|
||||
<View className="bg-white px-7 py-9 rounded-2xl min-h-[300px]">
|
||||
<View className="bg-white dark:bg-neutral-900 px-7 py-9 rounded-2xl min-h-[300px]">
|
||||
<Image
|
||||
source={images.check}
|
||||
alt="Check"
|
||||
alt={t("auth.signUp.verify.checkAlt")}
|
||||
className="w-[110px] h-[110px] mx-auto my-5"
|
||||
/>
|
||||
|
||||
<Text className="text-3xl font-JakartaBold text-center">
|
||||
Verified
|
||||
<Text className="text-3xl font-JakartaBold text-center text-black dark:text-white">
|
||||
{t("auth.signUp.verified.title")}
|
||||
</Text>
|
||||
|
||||
<Text className="text-base text-gray-400 font-Jakarta text-center mt-2">
|
||||
You've succesfully verified your account.
|
||||
<Text className="text-base text-gray-400 dark:text-neutral-500 font-Jakarta text-center mt-2">
|
||||
{t("auth.signUp.verified.body")}
|
||||
</Text>
|
||||
|
||||
<CustomButton
|
||||
title="Browse Home"
|
||||
title={t("common.browseHome")}
|
||||
onPress={() => router.push("/")}
|
||||
className="mt-5"
|
||||
/>
|
||||
@@ -382,4 +385,4 @@ const SignUp = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
export default SignUp;
|
||||
+13
-9
@@ -6,25 +6,29 @@ import Swiper from "react-native-swiper";
|
||||
|
||||
import { CustomButton } from "@/components/custom-button";
|
||||
import { onboarding } from "@/constants";
|
||||
import { useT } from "@/lib/i18n";
|
||||
|
||||
const Welcome = () => {
|
||||
const swiperRef = useRef<Swiper>(null);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const isLastSlide = activeIndex === onboarding.length - 1;
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex h-full items-center justify-between bg-white">
|
||||
<SafeAreaView className="flex h-full items-center justify-between bg-white dark:bg-neutral-950">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/(auth)/sign-up")}
|
||||
className="w-full flex justify-end items-end p-5"
|
||||
>
|
||||
<Text className="text-black text-base font-JakartaBold">Skip</Text>
|
||||
<Text className="text-black dark:text-white text-base font-JakartaBold">
|
||||
{t("onboarding.skip")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Swiper
|
||||
ref={swiperRef}
|
||||
loop={false}
|
||||
dot={<View className="w-8 h-1 mx-1 bg-[#E2E8F0] rounded-full" />}
|
||||
dot={<View className="w-8 h-1 mx-1 bg-[#E2E8F0] dark:bg-neutral-700 rounded-full" />}
|
||||
activeDot={<View className="w-8 h-1 mx-1 bg-[#0286FF] rounded-full" />}
|
||||
index={activeIndex}
|
||||
onIndexChanged={setActiveIndex}
|
||||
@@ -39,13 +43,13 @@ const Welcome = () => {
|
||||
/>
|
||||
|
||||
<View className="flex flex-row items-center justify-center w-full mt-10">
|
||||
<Text className="text-black text-3xl font-bold mx-10 text-center">
|
||||
{item.title}
|
||||
<Text className="text-black dark:text-white text-3xl font-bold mx-10 text-center">
|
||||
{t(item.titleKey)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="text-base font-JakartaSemiBold text-center text-[#858585] mx-10 mt-3">
|
||||
{item.description}
|
||||
<Text className="text-base font-JakartaSemiBold text-center text-[#858585] dark:text-neutral-400 mx-10 mt-3">
|
||||
{t(item.descKey)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
@@ -57,11 +61,11 @@ const Welcome = () => {
|
||||
? router.push("/(auth)/sign-up")
|
||||
: swiperRef.current?.scrollBy(1)
|
||||
}
|
||||
title={isLastSlide ? "Get Started" : "Next"}
|
||||
title={isLastSlide ? t("onboarding.getStarted") : t("onboarding.next")}
|
||||
className="w-11/12 mt-10"
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
||||
export default Welcome;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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'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
@@ -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
@@ -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'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 1–8.");
|
||||
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")}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -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'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'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;
|
||||
+16
-10
@@ -1,11 +1,13 @@
|
||||
import { useFonts } from "expo-font";
|
||||
import { Stack } from "expo-router";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { useEffect } from "react";
|
||||
import "react-native-reanimated";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n";
|
||||
import { SessionProvider } from "@/lib/session";
|
||||
import { SettingsProvider } from "@/lib/settings-provider";
|
||||
import { ThemeProvider } from "@/lib/theme";
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
@@ -32,15 +34,19 @@ const RootLayout = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(root)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
|
||||
<StatusBar style="dark" />
|
||||
</SessionProvider>
|
||||
<SettingsProvider>
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<SessionProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(root)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</SessionProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
</SettingsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ const App = () => {
|
||||
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-neutral-950">
|
||||
<ActivityIndicator size="large" color="#0286FF" />
|
||||
</View>
|
||||
);
|
||||
@@ -36,7 +36,7 @@ const App = () => {
|
||||
// Still loading the user's role from the database.
|
||||
if (role === undefined) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-neutral-950">
|
||||
<ActivityIndicator size="large" color="#0286FF" />
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user