import { MaterialCommunityIcons } from "@expo/vector-icons"; import { ActivityIndicator, Image, Text, TouchableOpacity, View, } from "react-native"; import { SERVICES } from "@/constants/services"; import { driverPhotoUri } from "@/lib/driver-photo"; import { useT } from "@/lib/i18n"; import type { RideOffer } from "@/types/type"; // The drivers who have volunteered for a request, and the rider's choice // between them. // // Dispatch broadcasts the job and this is what comes back: several drivers, // none of them assigned, each waiting to be picked. So every row has to carry // what a person actually decides on — how far away they are, how they're // rated, what they drive — and picking one has to be a single deliberate tap, // because that tap is what commits the rider and releases everyone else. // Rough road-speed assumption for turning a straight-line distance into // minutes. A per-offer Directions call would be more accurate and would also // mean one billed request per driver per poll; this is honest to within a // couple of minutes in city traffic, which is the precision a rider comparing // three drivers is actually using. const URBAN_KMH = 22; // Streets aren't straight. Multiplying the great-circle distance gets closer // to the distance a car really drives. const ROAD_FACTOR = 1.3; const etaMinutes = (meters: number | null): number | null => { if (meters === null || !Number.isFinite(meters)) return null; return Math.max( 1, Math.round(((meters * ROAD_FACTOR) / 1000 / URBAN_KMH) * 60), ); }; const distanceLabel = (meters: number | null): string | null => { if (meters === null || !Number.isFinite(meters)) return null; return meters < 1000 ? `${Math.round(meters / 50) * 50} m` : `${(meters / 1000).toFixed(1)} km`; }; type Props = { offers: RideOffer[]; /** Offer currently being taken, so only that row shows a spinner. */ pendingOfferId: number | null; busy: boolean; onPick: (offer: RideOffer) => void; }; export const OfferList = ({ offers, pendingOfferId, busy, onPick }: Props) => { const t = useT(); // What the rider is getting into. A driver who never filled in their car // model would otherwise leave the vehicle line blank on the one screen where // the rider is choosing between cars, so the service they drive for stands // in — "Car · 4 seats" is thin, but it isn't nothing. const vehicle = (offer: RideOffer): string => { const service = SERVICES.find((s) => s.id === offer.service); const label = offer.car_model ?? (service ? t(service.labelKey) : null); const seats = offer.car_seats ? t("bookRide.offers.seats", undefined, offer.car_seats) : null; return [label, seats].filter(Boolean).join(" · "); }; return ( {t("bookRide.offers.title")} {t("bookRide.offers.count", undefined, offers.length)} {offers.map((offer) => { const name = [offer.first_name, offer.last_name] .filter(Boolean) .join(" "); const distance = offer.pickup_distance_m ?? null; const eta = etaMinutes(distance); const taking = pendingOfferId === offer.offer_id; // The face the rider is choosing between. This is the screen the // driver's photo exists for, so it leads the row. const photo = driverPhotoUri(offer.profile_image_url); return ( {photo ? ( ) : ( )} {name || t("bookRide.match.driverFallback")} {offer.rating != null ? Number(offer.rating).toFixed(1) : t("bookRide.ratingFallback")} {vehicle(offer) ? ( {vehicle(offer)} ) : null} {eta !== null ? ( {t("bookRide.offers.away", { eta, distance: distanceLabel(distance) ?? "", })} ) : null} onPick(offer)} disabled={busy} className={`rounded-full px-5 py-2.5 ml-2 ${ busy && !taking ? "bg-emerald-500/40" : "bg-emerald-500" }`} > {taking ? ( ) : ( {t("bookRide.offers.pick")} )} ); })} ); };