Waseel: driver app, dispatch, POI suggestions, map fixes
This commit is contained in:
@@ -11,7 +11,7 @@ const getBgVariantStyle = (variant: ButtonProps["bgVariant"]) => {
|
||||
case "success":
|
||||
return "bg-emerald-500";
|
||||
case "outline":
|
||||
return "bg-transparent-500 border-neutral-300 border-[0.5px]";
|
||||
return "bg-transparent border-neutral-300 dark:border-neutral-700 border-[0.5px]";
|
||||
default:
|
||||
return "bg-[#0286ff]";
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const getBgVariantStyle = (variant: ButtonProps["bgVariant"]) => {
|
||||
const getTextVariantStyle = (variant: ButtonProps["textVariant"]) => {
|
||||
switch (variant) {
|
||||
case "primary":
|
||||
return "text-black";
|
||||
return "text-black dark:text-white";
|
||||
case "secondary":
|
||||
return "text-gray-100";
|
||||
case "danger":
|
||||
@@ -44,7 +44,7 @@ export const CustomButton = ({
|
||||
}: ButtonProps) => (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
className={`w-full rounded-full p-3 flex flex-row justify-center items-center shadow-md shadow-neutral-400/70 ${getBgVariantStyle(bgVariant)} ${className}`}
|
||||
className={`w-full rounded-full p-3 flex flex-row justify-center items-center shadow-md shadow-neutral-400/70 dark:shadow-neutral-950/70 ${getBgVariantStyle(bgVariant)} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{IconLeft && <IconLeft />}
|
||||
|
||||
+16
-13
@@ -1,6 +1,7 @@
|
||||
import { Image, Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { icons } from "@/constants";
|
||||
import { tr } from "@/lib/i18n";
|
||||
import { formatTime } from "@/lib/utils";
|
||||
import { DriverCardProps } from "@/types/type";
|
||||
|
||||
@@ -13,56 +14,58 @@ export const DriverCard = ({
|
||||
<TouchableOpacity
|
||||
onPress={setSelected}
|
||||
className={`${
|
||||
selected === item.id ? "bg-general-600" : "bg-white"
|
||||
selected === item.id
|
||||
? "bg-general-600 dark:bg-primary-500/20"
|
||||
: "bg-white dark:bg-neutral-900"
|
||||
} flex flex-row items-center justify-between py-5 px-3 rounded-xl`}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.profile_image_url }}
|
||||
alt="Driver Avatar"
|
||||
alt={tr("components.driverCard.avatarAlt")}
|
||||
className="w-14 h-14 rounded-full"
|
||||
/>
|
||||
|
||||
<View className="flex-1 flex flex-col items-start justify-center mx-3">
|
||||
<View className="flex flex-row items-center justify-start mb-1">
|
||||
<Text className="text-lg font-JakartaRegular">
|
||||
<Text className="text-lg font-JakartaRegular text-black dark:text-white">
|
||||
{item.title ?? `${item.first_name} ${item.last_name}`}
|
||||
</Text>
|
||||
|
||||
<View className="flex flex-row items-center space-x-1 ml-2">
|
||||
<Image source={icons.star} alt="Star" className="w-3.5 h-3.5" />
|
||||
<Text className="text-sm font-JakartaRegular">{item.rating}</Text>
|
||||
<Image source={icons.star} alt={tr("components.driverCard.starAlt")} className="w-3.5 h-3.5" />
|
||||
<Text className="text-sm font-JakartaRegular text-black dark:text-white">{item.rating}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-row items-center justify-start">
|
||||
<View className="flex flex-row items-center">
|
||||
<Image source={icons.dollar} alt="Dollar" className="w-4 h-4" />
|
||||
<Text className="text-sm font-JakartaRegular ml-1">
|
||||
<Image source={icons.dollar} alt={tr("components.driverCard.dollarAlt")} className="w-4 h-4" />
|
||||
<Text className="text-sm font-JakartaRegular ml-1 text-black dark:text-white">
|
||||
${item.price}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="text-sm font-JakartaRegular text-general-800 mx-1">
|
||||
<Text className="text-sm font-JakartaRegular text-general-800 dark:text-neutral-400 mx-1">
|
||||
|
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm font-JakartaRegular text-general-800">
|
||||
<Text className="text-sm font-JakartaRegular text-general-800 dark:text-neutral-400">
|
||||
{formatTime(parseInt(`${item.time}`))}
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm font-JakartaRegular text-general-800 mx-1">
|
||||
<Text className="text-sm font-JakartaRegular text-general-800 dark:text-neutral-400 mx-1">
|
||||
|
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm font-JakartaRegular text-general-800">
|
||||
{item.car_seats} seats
|
||||
<Text className="text-sm font-JakartaRegular text-general-800 dark:text-neutral-400">
|
||||
{tr("components.driverCard.seats", {}, item.car_seats)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Image
|
||||
source={{ uri: item.car_image_url }}
|
||||
alt="Car"
|
||||
alt={tr("components.driverCard.carAlt")}
|
||||
className="h-14 w-14"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from "react-native";
|
||||
|
||||
import { icons } from "@/constants";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import type { GoogleInputProps } from "@/types/type";
|
||||
|
||||
const googleApiKey = process.env.EXPO_PUBLIC_GOOGLE_API_KEY!;
|
||||
@@ -69,10 +71,15 @@ export const GoogleTextInput = ({
|
||||
textInputBackgroundColor,
|
||||
handlePress,
|
||||
}: GoogleInputProps) => {
|
||||
const t = useT();
|
||||
const { isDark } = useTheme();
|
||||
const [query, setQuery] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const inputBg = textInputBackgroundColor || (isDark ? "#1a1a1a" : "white");
|
||||
const inputShadow = isDark ? "#000000" : "#d4d4d4";
|
||||
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
|
||||
@@ -118,14 +125,14 @@ export const GoogleTextInput = ({
|
||||
<View
|
||||
className="flex flex-row items-center rounded-full px-4 mt-1"
|
||||
style={{
|
||||
backgroundColor: textInputBackgroundColor || "white",
|
||||
shadowColor: "#d4d4d4",
|
||||
backgroundColor: inputBg,
|
||||
shadowColor: inputShadow,
|
||||
}}
|
||||
>
|
||||
<View className="justify-center items-center w-6 h-6">
|
||||
<Image
|
||||
source={icon ? icon : icons.search}
|
||||
alt="Search"
|
||||
alt={t("components.googleTextInput.searchAlt")}
|
||||
className="w-6 h-6"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
@@ -134,9 +141,9 @@ export const GoogleTextInput = ({
|
||||
<TextInput
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder={initialLocation ?? "Where do you want to go?"}
|
||||
placeholderTextColor="gray"
|
||||
className="flex-1 p-3 text-base font-JakartaSemiBold"
|
||||
placeholder={initialLocation ?? t("components.googleTextInput.placeholder")}
|
||||
placeholderTextColor="#a3a3a3"
|
||||
className="flex-1 p-3 text-base font-JakartaSemiBold text-black dark:text-white"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -144,8 +151,8 @@ export const GoogleTextInput = ({
|
||||
<View
|
||||
className="rounded-xl mt-1"
|
||||
style={{
|
||||
backgroundColor: textInputBackgroundColor || "white",
|
||||
shadowColor: "#d4d4d4",
|
||||
backgroundColor: inputBg,
|
||||
shadowColor: inputShadow,
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
@@ -155,9 +162,9 @@ export const GoogleTextInput = ({
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => onSelect(item)}
|
||||
className="p-3 border-b border-general-700"
|
||||
className="p-3 border-b border-general-700 dark:border-neutral-700"
|
||||
>
|
||||
<Text className="text-base font-JakartaRegular">
|
||||
<Text className="text-base font-JakartaRegular text-black dark:text-white">
|
||||
{item.text}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
import { tr } from "@/lib/i18n";
|
||||
import type { InputFieldProps } from "@/types/type";
|
||||
|
||||
export const InputField = ({
|
||||
@@ -25,26 +26,29 @@ export const InputField = ({
|
||||
<KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"}>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View className="my-2 w-full">
|
||||
<Text className={`text-lg font-JakartaSemiBold mb-3 ${labelStyles}`}>
|
||||
<Text
|
||||
className={`text-lg font-JakartaSemiBold mb-3 text-black dark:text-white ${labelStyles}`}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
<View
|
||||
className={`flex flex-row justify-start items-center relative bg-neutral-100 rounded-full border border-neutral-100 focus:border-primary-500 ${containerStyles}`}
|
||||
className={`flex flex-row justify-start items-center relative bg-neutral-100 dark:bg-neutral-800 rounded-full border border-neutral-100 dark:border-neutral-800 focus:border-primary-500 ${containerStyles}`}
|
||||
>
|
||||
{icon && (
|
||||
<Image
|
||||
source={icon}
|
||||
alt={`${label} icon`}
|
||||
alt={tr("components.inputField.labelIconAlt", { label })}
|
||||
className={`h-6 w-6 ml-4 mt-1 ${iconStyles}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
className={`rounded-full p-4 font-JakartaSemiBold text-[15px] flex-1 text-left ${inputStyles}`}
|
||||
className={`rounded-full p-4 font-JakartaSemiBold text-[15px] flex-1 text-left text-black dark:text-white ${inputStyles}`}
|
||||
secureTextEntry={secureTextEntry}
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
placeholderTextColor="#a3a3a3"
|
||||
selectionColor="#0286ff"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Linking, Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { tr } from "@/lib/i18n";
|
||||
import type { LocationStatus } from "@/lib/use-user-location";
|
||||
|
||||
const COPY: Record<string, { title: string; body: string; action: string }> = {
|
||||
const COPY: Record<string, { titleKey: string; bodyKey: string; actionKey: string }> = {
|
||||
denied: {
|
||||
title: "Location access is off",
|
||||
body: "Waseel needs your location to show nearby drivers and set your pickup point.",
|
||||
action: "Open Settings",
|
||||
titleKey: "components.locationNotice.denied.title",
|
||||
bodyKey: "components.locationNotice.denied.body",
|
||||
actionKey: "components.locationNotice.denied.action",
|
||||
},
|
||||
"services-off": {
|
||||
title: "Location services are off",
|
||||
body: "Turn on location on your device, then try again.",
|
||||
action: "Try Again",
|
||||
titleKey: "components.locationNotice.servicesOff.title",
|
||||
bodyKey: "components.locationNotice.servicesOff.body",
|
||||
actionKey: "components.locationNotice.servicesOff.action",
|
||||
},
|
||||
unavailable: {
|
||||
title: "Couldn't find your location",
|
||||
body: "Move somewhere with a clearer signal, or set your pickup point manually.",
|
||||
action: "Try Again",
|
||||
titleKey: "components.locationNotice.unavailable.title",
|
||||
bodyKey: "components.locationNotice.unavailable.body",
|
||||
actionKey: "components.locationNotice.unavailable.action",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,12 +35,12 @@ export const LocationNotice = ({
|
||||
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center px-6">
|
||||
<Text className="text-base font-JakartaBold text-black text-center">
|
||||
{copy.title}
|
||||
<Text className="text-base font-JakartaBold text-black dark:text-white text-center">
|
||||
{tr(copy.titleKey)}
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm font-Jakarta text-general-200 text-center mt-2">
|
||||
{copy.body}
|
||||
<Text className="text-sm font-Jakarta text-general-200 dark:text-neutral-400 text-center mt-2">
|
||||
{tr(copy.bodyKey)}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -50,9 +51,9 @@ export const LocationNotice = ({
|
||||
className="mt-5 rounded-full bg-primary-500 px-6 py-3"
|
||||
>
|
||||
<Text className="text-white font-JakartaBold text-sm">
|
||||
{copy.action}
|
||||
{tr(copy.actionKey)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
};
|
||||
+26
-4
@@ -1,15 +1,17 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
import MapView, { Marker, PROVIDER_DEFAULT } from "react-native-maps";
|
||||
import MapViewDirections from "react-native-maps-directions";
|
||||
|
||||
import { icons } from "@/constants";
|
||||
import { useFetch } from "@/lib/fetch";
|
||||
import { tr } from "@/lib/i18n";
|
||||
import {
|
||||
calculateDriverTimes,
|
||||
calculateRegion,
|
||||
generateMarkersFromData,
|
||||
} from "@/lib/map";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { useDriverStore, useLocationStore, useServiceStore } from "@/store";
|
||||
import type { Driver, MarkerData } from "@/types/type";
|
||||
|
||||
@@ -48,6 +50,7 @@ export const Map = () => {
|
||||
} = useLocationStore();
|
||||
const { service } = useServiceStore();
|
||||
const { selectedDriver, setDrivers } = useDriverStore();
|
||||
const { isDark } = useTheme();
|
||||
|
||||
// Online drivers of the selected service near the rider. Falls back to a
|
||||
// Beirut center when the rider's position isn't resolved yet so the map
|
||||
@@ -59,6 +62,7 @@ export const Map = () => {
|
||||
);
|
||||
|
||||
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
||||
const mapRef = useRef<MapView>(null);
|
||||
|
||||
const region = calculateRegion({
|
||||
userLatitude,
|
||||
@@ -67,6 +71,23 @@ export const Map = () => {
|
||||
destinationLongitude,
|
||||
});
|
||||
|
||||
// `initialRegion` is read once, at mount. The map mounts before the location
|
||||
// fix arrives, so it would sit on the Beirut fallback forever and never zoom
|
||||
// out to fit a destination the rider picks later. Animate on every real
|
||||
// change instead. Keyed on the coordinates so the repeated setUserLocation
|
||||
// from reverse geocoding (same coords, new address) doesn't yank the camera
|
||||
// back while the rider is panning.
|
||||
const regionKey = `${region.latitude},${region.longitude},${region.latitudeDelta},${region.longitudeDelta}`;
|
||||
const lastRegionKey = useRef(regionKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastRegionKey.current === regionKey) return;
|
||||
|
||||
lastRegionKey.current = regionKey;
|
||||
mapRef.current?.animateToRegion(region, 500);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [regionKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(drivers)) {
|
||||
if (!userLatitude || !userLongitude) return;
|
||||
@@ -113,15 +134,16 @@ export const Map = () => {
|
||||
|
||||
return (
|
||||
<MapView
|
||||
ref={mapRef}
|
||||
provider={PROVIDER_DEFAULT}
|
||||
style={styles.map}
|
||||
tintColor="black"
|
||||
tintColor={isDark ? "white" : "black"}
|
||||
mapType={MAP_TYPE}
|
||||
customMapStyle={MUTED_POI_STYLE}
|
||||
showsPointsOfInterest={false}
|
||||
initialRegion={region}
|
||||
showsUserLocation
|
||||
userInterfaceStyle="light"
|
||||
userInterfaceStyle={isDark ? "dark" : "light"}
|
||||
>
|
||||
{markers.map((marker) => (
|
||||
<Marker
|
||||
@@ -148,7 +170,7 @@ export const Map = () => {
|
||||
latitude: destinationLatitude,
|
||||
longitude: destinationLongitude,
|
||||
}}
|
||||
title="Destination"
|
||||
title={tr("components.map.destination")}
|
||||
image={icons.pin}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { useT } from "@/lib/i18n";
|
||||
|
||||
// react-native-maps does not support web. This stub keeps the web bundle
|
||||
// working for local testing; use a native build for real map functionality.
|
||||
export const Map = () => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<View className="w-full h-full rounded-2xl bg-general-100 flex items-center justify-center">
|
||||
<Text className="text-general-200 text-center font-JakartaMedium">
|
||||
Map is not available on web.{"\n"}Run on Android/iOS for the full
|
||||
experience.
|
||||
{t("components.map.webUnavailable")}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { POI_CATEGORIES, searchNearby } from "@/lib/places";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useLocationStore } from "@/store";
|
||||
import type { NearbyPlace } from "@/types/type";
|
||||
|
||||
@@ -19,6 +20,7 @@ type ChipState =
|
||||
export const NearbySuggestions = () => {
|
||||
const { userLatitude, userLongitude, setDestinationLocation } =
|
||||
useLocationStore();
|
||||
const t = useT();
|
||||
const [chips, setChips] = useState<Record<string, ChipState>>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,8 +59,8 @@ export const NearbySuggestions = () => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text className="text-base font-JakartaSemiBold mb-3">
|
||||
Nearby suggestions
|
||||
<Text className="text-base font-JakartaSemiBold mb-3 text-black dark:text-white">
|
||||
{t("pois.nearbyTitle")}
|
||||
</Text>
|
||||
|
||||
<ScrollView
|
||||
@@ -79,7 +81,7 @@ export const NearbySuggestions = () => {
|
||||
className={`flex-row items-center rounded-2xl border px-3 py-2.5 ${
|
||||
ready
|
||||
? "border-primary-500 bg-primary-500/10"
|
||||
: "border-neutral-200 bg-neutral-100"
|
||||
: "border-neutral-200 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-800"
|
||||
}`}
|
||||
style={{ minWidth: 150 }}
|
||||
>
|
||||
@@ -96,19 +98,36 @@ export const NearbySuggestions = () => {
|
||||
}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{category.label}
|
||||
{t(category.labelKey)}
|
||||
</Text>
|
||||
<Text
|
||||
className="text-[11px] text-general-200"
|
||||
className="text-[11px] text-general-200 dark:text-neutral-400"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{!state || state.status === "loading"
|
||||
? "searching…"
|
||||
? t("pois.searching")
|
||||
: state.status === "empty"
|
||||
? "none nearby"
|
||||
: state.place.distanceMeters != null
|
||||
? `${Math.round(state.place.distanceMeters / 100) / 10} km away`
|
||||
: state.place.name}
|
||||
? t("pois.noneNearby")
|
||||
: state.place.routeDistanceMeters != null
|
||||
? t("pois.routeAway", {
|
||||
km:
|
||||
Math.round(
|
||||
state.place.routeDistanceMeters / 100,
|
||||
) / 10,
|
||||
min: Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
(state.place.routeDurationSeconds ?? 0) / 60,
|
||||
),
|
||||
),
|
||||
})
|
||||
: state.place.distanceMeters != null
|
||||
? t("pois.kmAway", {
|
||||
km:
|
||||
Math.round(state.place.distanceMeters / 100) /
|
||||
10,
|
||||
})
|
||||
: state.place.name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -117,4 +136,4 @@ export const NearbySuggestions = () => {
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+10
-8
@@ -5,6 +5,7 @@ import { Image, Text, View, Alert } from "react-native";
|
||||
|
||||
import { icons } from "@/constants";
|
||||
import { googleAuth } from "@/lib/auth";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useSession } from "@/lib/session";
|
||||
|
||||
import { CustomButton } from "./custom-button";
|
||||
@@ -39,6 +40,7 @@ function GoogleOAuth({
|
||||
androidClientId?: string;
|
||||
}) {
|
||||
const { setSession } = useSession();
|
||||
const t = useT();
|
||||
|
||||
const [request, response, promptAsync] = Google.useIdTokenAuthRequest({
|
||||
clientId,
|
||||
@@ -52,7 +54,7 @@ function GoogleOAuth({
|
||||
const idToken = response.params?.id_token;
|
||||
|
||||
if (!idToken) {
|
||||
Alert.alert("Google sign-in failed", "No token returned. Try again.");
|
||||
Alert.alert(t("components.oauth.alertFailTitle"), t("components.oauth.alertFailNoToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,12 +65,12 @@ function GoogleOAuth({
|
||||
} catch (err: any) {
|
||||
console.error("OAuth error", err);
|
||||
Alert.alert(
|
||||
"Google sign-in failed",
|
||||
err?.message || "Please try again.",
|
||||
t("components.oauth.alertFailTitle"),
|
||||
err?.message || t("components.oauth.alertFailFallback"),
|
||||
);
|
||||
}
|
||||
})();
|
||||
}, [response, setSession]);
|
||||
}, [response, setSession, t]);
|
||||
|
||||
const handleGoogleOAuth = useCallback(() => {
|
||||
void promptAsync();
|
||||
@@ -77,11 +79,11 @@ function GoogleOAuth({
|
||||
return (
|
||||
<View>
|
||||
<View className="flex flex-row justify-center items-center mt-4 gap-x-3">
|
||||
<View className="flex-1 h-px bg-general-100" />
|
||||
<View className="flex-1 h-px bg-general-100 dark:bg-neutral-700" />
|
||||
|
||||
<Text className="text-lg">Or</Text>
|
||||
<Text className="text-lg text-black dark:text-white">{t("components.oauth.or")}</Text>
|
||||
|
||||
<View className="flex-1 h-px bg-general-100" />
|
||||
<View className="flex-1 h-px bg-general-100 dark:bg-neutral-700" />
|
||||
</View>
|
||||
|
||||
<CustomButton
|
||||
@@ -90,7 +92,7 @@ function GoogleOAuth({
|
||||
iconLeft={() => (
|
||||
<Image
|
||||
source={icons.google}
|
||||
alt="Google logo"
|
||||
alt={t("components.oauth.googleLogoAlt")}
|
||||
resizeMode="contain"
|
||||
className="h-5 w-5 mx-2"
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppState, Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { InputField } from "@/components/input-field";
|
||||
import { icons } from "@/constants";
|
||||
import { tr } from "@/lib/i18n";
|
||||
|
||||
// `\b` won't match between two digits, so a longer run like an order number
|
||||
// never yields a false positive.
|
||||
@@ -33,7 +34,7 @@ type OtpFieldProps = {
|
||||
* 3. Typing it.
|
||||
*/
|
||||
export const OtpField = ({
|
||||
label = "Code",
|
||||
label = tr("components.otp.code"),
|
||||
value,
|
||||
onChange,
|
||||
onComplete,
|
||||
@@ -105,7 +106,7 @@ export const OtpField = ({
|
||||
<InputField
|
||||
label={label}
|
||||
icon={icons.lock}
|
||||
placeholder="123456"
|
||||
placeholder={tr("components.otp.codePlaceholder")}
|
||||
value={value}
|
||||
onChangeText={handleChange}
|
||||
keyboardType="number-pad"
|
||||
@@ -125,7 +126,7 @@ export const OtpField = ({
|
||||
className="self-start mt-2 rounded-full bg-primary-500/10 px-4 py-2"
|
||||
>
|
||||
<Text className="text-primary-500 font-JakartaSemiBold text-sm">
|
||||
Paste code from Gmail
|
||||
{tr("components.otp.pasteCode")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
+41
-34
@@ -6,6 +6,7 @@ import ReactNativeModal from "react-native-modal";
|
||||
|
||||
import { images } from "@/constants";
|
||||
import { ApiError, fetchAPI } from "@/lib/fetch";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { formatLBP } from "@/lib/pricing";
|
||||
import { useLocationStore } from "@/store";
|
||||
import type { PaymentProps } from "@/types/type";
|
||||
@@ -32,6 +33,7 @@ export const Payment = ({
|
||||
const [method, setMethod] = useState<PaymentMethod>("cash");
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const t = useT();
|
||||
|
||||
const fareCents = Math.round(parseFloat(amount) * 100); // in cents
|
||||
|
||||
@@ -66,8 +68,8 @@ export const Payment = ({
|
||||
} catch (err) {
|
||||
console.log("[PAYMENT]: ", err);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Something went wrong while booking your ride. Please try again.",
|
||||
t("components.payment.alertErrorTitle"),
|
||||
t("components.payment.alertErrorBody"),
|
||||
);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
@@ -138,8 +140,8 @@ export const Payment = ({
|
||||
setSuccess(true);
|
||||
} else {
|
||||
Alert.alert(
|
||||
"Payment not completed",
|
||||
"Your payment was cancelled or could not be verified. Please try again.",
|
||||
t("components.payment.alertPaymentNotCompletedTitle"),
|
||||
t("components.payment.alertPaymentNotCompletedBody"),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -149,13 +151,13 @@ export const Payment = ({
|
||||
// branch every cancellation lands in the generic "something went wrong".
|
||||
if (err instanceof ApiError && err.status === 400) {
|
||||
Alert.alert(
|
||||
"Payment not completed",
|
||||
"Your payment was cancelled or could not be verified. Please try again.",
|
||||
t("components.payment.alertPaymentNotCompletedTitle"),
|
||||
t("components.payment.alertPaymentNotCompletedBody"),
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Something went wrong while processing your payment. Please try again.",
|
||||
t("components.payment.alertProcessingTitle"),
|
||||
t("components.payment.alertProcessingBody"),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -166,15 +168,19 @@ export const Payment = ({
|
||||
const confirm = () =>
|
||||
method === "cash"
|
||||
? payWithCash()
|
||||
: Alert.alert("Pay by card", `Your card will be charged $${amount}.`, [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{ text: "Continue", onPress: () => void payWithCard() },
|
||||
]);
|
||||
: Alert.alert(
|
||||
t("components.payment.alertPayCardTitle"),
|
||||
t("components.payment.alertPayCardBody", { amount }),
|
||||
[
|
||||
{ text: t("common.cancel"), style: "cancel" },
|
||||
{ text: t("common.continue"), onPress: () => void payWithCard() },
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text className="text-lg font-JakartaSemiBold mt-4 mb-2">
|
||||
Payment Method
|
||||
<Text className="text-lg font-JakartaSemiBold mt-4 mb-2 text-black dark:text-white">
|
||||
{t("components.payment.paymentMethod")}
|
||||
</Text>
|
||||
|
||||
<View className="flex flex-row gap-x-3">
|
||||
@@ -182,16 +188,16 @@ export const Payment = ({
|
||||
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("components.payment.cash")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -199,16 +205,16 @@ export const Payment = ({
|
||||
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("components.payment.card")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -216,10 +222,10 @@ export const Payment = ({
|
||||
<CustomButton
|
||||
title={
|
||||
processing
|
||||
? "Processing..."
|
||||
? t("components.payment.processing")
|
||||
: method === "cash"
|
||||
? "Book ride · Pay cash to driver"
|
||||
: "Confirm & Pay by Card"
|
||||
? t("components.payment.bookCash")
|
||||
: t("components.payment.confirmCard")
|
||||
}
|
||||
className="my-2 mt-4"
|
||||
onPress={confirm}
|
||||
@@ -230,23 +236,24 @@ export const Payment = ({
|
||||
isVisible={success}
|
||||
onBackdropPress={() => setSuccess(false)}
|
||||
>
|
||||
<View className="flex flex-col items-center justify-center bg-white p-7 rounded-2xl">
|
||||
<Image source={images.check} alt="Check" className="w-28 h-28 mt-5" />
|
||||
<View className="flex flex-col items-center justify-center bg-white dark:bg-neutral-900 p-7 rounded-2xl">
|
||||
<Image source={images.check} alt={t("components.payment.checkAlt")} className="w-28 h-28 mt-5" />
|
||||
|
||||
<Text className="text-2xl text-center font-JakartaBold mt-5">
|
||||
Ride Booked!
|
||||
<Text className="text-2xl text-center font-JakartaBold mt-5 text-black dark:text-white">
|
||||
{t("components.payment.rideBooked")}
|
||||
</Text>
|
||||
|
||||
<Text className="text-base text-general-200 text-JakartaMedium text-center mt-3">
|
||||
Thank you for your booking.{"\n"} Your reservation has been placed.
|
||||
{"\n"}
|
||||
<Text className="text-base text-general-200 dark:text-neutral-400 text-JakartaMedium text-center mt-3">
|
||||
{t("components.payment.successBody")}
|
||||
{method === "cash"
|
||||
? `Please have ${formatLBP(parseFloat(amount))} ready.`
|
||||
? t("components.payment.cashInstruction", {
|
||||
lbp: formatLBP(parseFloat(amount)),
|
||||
})
|
||||
: null}
|
||||
</Text>
|
||||
|
||||
<CustomButton
|
||||
title="Back Home"
|
||||
title={t("components.payment.backHome")}
|
||||
onPress={() => {
|
||||
setSuccess(false);
|
||||
router.push("/(root)/(tabs)/home");
|
||||
|
||||
+26
-25
@@ -1,6 +1,7 @@
|
||||
import { Image, Text, View } from "react-native";
|
||||
|
||||
import { icons } from "@/constants";
|
||||
import { tr } from "@/lib/i18n";
|
||||
import { formatDate, formatTime } from "@/lib/utils";
|
||||
import type { Ride } from "@/types/type";
|
||||
|
||||
@@ -17,22 +18,22 @@ export const RideCard = ({ ride }: { ride: Ride }) => {
|
||||
} = ride;
|
||||
|
||||
return (
|
||||
<View className="flex flex-row items-center justify-center bg-white rounded-lg shadow-sm shadow-neutral-300 mb-3">
|
||||
<View className="flex flex-row items-center justify-center bg-white dark:bg-neutral-900 rounded-lg shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40 mb-3">
|
||||
<View className="flex flex-col items-center justify-center p-3">
|
||||
<View className="flex flex-row items-center justify-between">
|
||||
<Image
|
||||
source={{
|
||||
uri: `https://maps.geoapify.com/v1/staticmap?style=osm-bright&width=600&height=400¢er=lonlat:${destination_longitude},${destination_latitude}&zoom=14&apiKey=${process.env.EXPO_PUBLIC_GEOAPIFY_API_KEY}`,
|
||||
}}
|
||||
alt="Map"
|
||||
alt={tr("components.rideCard.mapAlt")}
|
||||
className="w-[80px] h-[90px] rounded-lg"
|
||||
/>
|
||||
|
||||
<View className="flex flex-col mx-5 gap-y-5 flex-1">
|
||||
<View className="flex flex-row items-center gap-x-2">
|
||||
<Image source={icons.to} alt="Origin" className="w-5 h-5" />
|
||||
<Image source={icons.to} alt={tr("components.rideCard.originAlt")} className="w-5 h-5" />
|
||||
|
||||
<Text className="font-JakartaMedium" numberOfLines={1}>
|
||||
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
|
||||
{origin_address}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -40,71 +41,71 @@ export const RideCard = ({ ride }: { ride: Ride }) => {
|
||||
<View className="flex flex-row items-center gap-x-2">
|
||||
<Image
|
||||
source={icons.point}
|
||||
alt="Destination"
|
||||
alt={tr("components.rideCard.destinationAlt")}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
|
||||
<Text className="font-JakartaMedium" numberOfLines={1}>
|
||||
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
|
||||
{destination_address}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-col w-full mt-5 bg-general-500 rounded-lg p-3 items-start justify-center">
|
||||
<View className="flex flex-col w-full mt-5 bg-general-500 dark:bg-neutral-800 rounded-lg p-3 items-start justify-center">
|
||||
<View className="flex flex-row items-center w-full justify-between mb-5">
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
Date & Time
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{tr("components.rideCard.dateTime")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{formatDate(created_at)}, {formatTime(ride_time)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-row items-center w-full justify-between mb-5">
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
Driver
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{tr("components.rideCard.driver")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{driver.first_name} {driver.last_name}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-row items-center w-full justify-between mb-5">
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
Car Seats
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{tr("components.rideCard.carSeats")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{driver.car_seats}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-row items-center w-full justify-between mb-5">
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
Fare
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{tr("components.rideCard.fare")}
|
||||
</Text>
|
||||
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
${(ride.fare_price / 100).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-row items-center w-full justify-between mb-5">
|
||||
<Text className="font-JakartaMedium text-gray-500 text-xs">
|
||||
Payment Status
|
||||
<Text className="font-JakartaMedium text-gray-500 dark:text-neutral-400 text-xs">
|
||||
{tr("components.rideCard.paymentStatus")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className={`font-JakartaMedium capitalize text-xs ${payment_status === "paid" ? "text-emerald-500" : "text-gray-500"}`}
|
||||
className={`font-JakartaMedium capitalize text-xs ${payment_status === "paid" ? "text-emerald-500 dark:text-emerald-400" : "text-gray-500 dark:text-neutral-400"}`}
|
||||
>
|
||||
{payment_status === "cash"
|
||||
? "Cash · Pay to driver"
|
||||
? tr("components.rideCard.paymentCash")
|
||||
: payment_status === "paid"
|
||||
? "Paid by card"
|
||||
: payment_status}
|
||||
? tr("components.rideCard.paymentPaid")
|
||||
: tr("components.rideCard.paymentOther", { status: payment_status })}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Image, Text, TouchableOpacity, View } from "react-native";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
|
||||
import { icons } from "@/constants";
|
||||
import { tr } from "@/lib/i18n";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
|
||||
import { Map } from "./map";
|
||||
|
||||
@@ -14,29 +16,32 @@ type RideLayoutProps = {
|
||||
};
|
||||
|
||||
export const RideLayout = ({
|
||||
title = "Go Back",
|
||||
title,
|
||||
snapPoints,
|
||||
children,
|
||||
}: PropsWithChildren<RideLayoutProps>) => {
|
||||
const bottomSheetRef = useRef<BottomSheet>(null);
|
||||
const { isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView>
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex flex-col h-screen bg-blue-500">
|
||||
<View className="flex-1 bg-white dark:bg-neutral-950">
|
||||
<View className="flex flex-col h-screen bg-blue-500 dark:bg-neutral-900">
|
||||
<View className="flex flex-row absolute z-10 top-16 items-center justify-start px-5">
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<View className="w-10 h-10 bg-white rounded-full items-center justify-center">
|
||||
<View className="w-10 h-10 bg-white dark:bg-neutral-900 rounded-full items-center justify-center">
|
||||
<Image
|
||||
source={icons.backArrow}
|
||||
alt="Back arrow"
|
||||
alt={tr("components.rideLayout.backArrowAlt")}
|
||||
resizeMode="contain"
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text className="text-xl font-JakartaSemiBold ml-5">{title}</Text>
|
||||
<Text className="text-xl font-JakartaSemiBold ml-5 text-black dark:text-white">
|
||||
{title ?? tr("components.rideLayout.goBack")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Map />
|
||||
@@ -47,6 +52,10 @@ export const RideLayout = ({
|
||||
ref={bottomSheetRef}
|
||||
snapPoints={snapPoints ?? ["40%", "85%"]}
|
||||
index={0}
|
||||
backgroundStyle={{ backgroundColor: isDark ? "#0a0a0a" : "#ffffff" }}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: isDark ? "#525252" : "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { SERVICES } from "@/constants/services";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useServiceStore } from "@/store";
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ import { useServiceStore } from "@/store";
|
||||
*/
|
||||
export const ServiceSelector = () => {
|
||||
const { service, setService } = useServiceStore();
|
||||
const t = useT();
|
||||
|
||||
const selected = SERVICES.find((item) => item.id === service);
|
||||
|
||||
@@ -33,7 +35,7 @@ export const ServiceSelector = () => {
|
||||
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 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-800"
|
||||
}`}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
@@ -45,10 +47,10 @@ export const ServiceSelector = () => {
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
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>
|
||||
);
|
||||
@@ -56,8 +58,8 @@ export const ServiceSelector = () => {
|
||||
</View>
|
||||
|
||||
{selected ? (
|
||||
<Text className="mt-3 text-sm font-Jakarta text-general-200">
|
||||
{selected.tagline}
|
||||
<Text className="mt-3 text-sm font-Jakarta text-general-200 dark:text-neutral-400">
|
||||
{t(selected.taglineKey)}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { Switch, Text, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { useTheme } from "@/lib/theme";
|
||||
|
||||
type IconName = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
||||
|
||||
type RightKind = "chevron" | "switch" | "value" | "none";
|
||||
|
||||
type SettingsRowProps = {
|
||||
icon: IconName;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
right?: RightKind;
|
||||
/** For `right: "value"` — the string shown on the trailing side. */
|
||||
value?: string;
|
||||
/** For `right: "switch"`. */
|
||||
switchValue?: boolean;
|
||||
onSwitchChange?: (value: boolean) => void;
|
||||
onPress?: () => void;
|
||||
/** Red accent — used for the emergency-call row. */
|
||||
danger?: boolean;
|
||||
};
|
||||
|
||||
/** A single row in the Settings screen. Born dark-aware. */
|
||||
export const SettingsRow = ({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
right = "none",
|
||||
value,
|
||||
switchValue,
|
||||
onSwitchChange,
|
||||
onPress,
|
||||
danger = false,
|
||||
}: SettingsRowProps) => {
|
||||
const { isDark } = useTheme();
|
||||
const interactive = right === "chevron" || right === "value";
|
||||
|
||||
const content = (
|
||||
<View className="flex-row items-center py-3.5">
|
||||
<View
|
||||
className={`w-10 h-10 rounded-full items-center justify-center mr-3.5 ${
|
||||
danger
|
||||
? "bg-rose-500/15"
|
||||
: "bg-neutral-100 dark:bg-neutral-800"
|
||||
}`}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
size={20}
|
||||
color={danger ? "#e11d48" : isDark ? "#e5e5e5" : "#404040"}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-[15px] font-JakartaSemiBold ${
|
||||
danger ? "text-rose-600 dark:text-rose-400" : "text-black dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{subtitle ? (
|
||||
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400 mt-0.5">
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{right === "switch" ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={onSwitchChange}
|
||||
trackColor={{ false: "#d4d4d4", true: "#0286ff" }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{right === "value" ? (
|
||||
<Text className="text-sm font-JakartaMedium text-general-200 dark:text-neutral-400 mr-1">
|
||||
{value}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{right === "chevron" ? (
|
||||
<MaterialCommunityIcons
|
||||
name="chevron-right"
|
||||
size={22}
|
||||
color={isDark ? "#737373" : "#a3a3a3"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (right === "switch" || !interactive || !onPress) {
|
||||
return <View className="px-4">{content}</View>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.6}
|
||||
className="px-4"
|
||||
>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user