Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
5.1 KiB
TypeScript
147 lines
5.1 KiB
TypeScript
import * as Location from "expo-location";
|
|
import { router } from "expo-router";
|
|
import { useMemo, useState } from "react";
|
|
import { Alert, Linking, ScrollView, View } from "react-native";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card } from "@/components/ui/Card";
|
|
import { AppHeader } from "@/components/ui/Header";
|
|
import { Screen } from "@/components/ui/Screen";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { branches } from "@/lib/data/branches";
|
|
import { routes } from "@/lib/routes";
|
|
import { useLocationStore } from "@/lib/stores/locationStore";
|
|
import { colors, gutter } from "@/lib/theme";
|
|
|
|
function dist(a: { lat: number; lng: number }, b: { lat: number; lng: number }) {
|
|
const toRad = (d: number) => (d * Math.PI) / 180;
|
|
const R = 6371;
|
|
const dLat = toRad(b.lat - a.lat);
|
|
const dLng = toRad(b.lng - a.lng);
|
|
const x =
|
|
Math.sin(dLat / 2) ** 2 +
|
|
Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * Math.sin(dLng / 2) ** 2;
|
|
return 2 * R * Math.asin(Math.sqrt(x));
|
|
}
|
|
|
|
export default function BranchesScreen() {
|
|
const { t } = useTranslation();
|
|
const setBranchId = useLocationStore((s) => s.setBranchId);
|
|
const setFulfillment = useLocationStore((s) => s.setFulfillment);
|
|
const setCoords = useLocationStore((s) => s.setCoords);
|
|
const branchId = useLocationStore((s) => s.branchId);
|
|
const [coords, setLocal] = useState<{ lat: number; lng: number } | null>(null);
|
|
|
|
const sorted = useMemo(() => {
|
|
if (!coords) return branches;
|
|
return [...branches].sort((a, b) => dist(coords, a) - dist(coords, b));
|
|
}, [coords]);
|
|
|
|
const useMyLocation = async () => {
|
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
if (status !== "granted") {
|
|
Alert.alert("Location", "Permission denied");
|
|
return;
|
|
}
|
|
const pos = await Location.getCurrentPositionAsync({});
|
|
const c = { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
|
setLocal(c);
|
|
setCoords(c.lat, c.lng);
|
|
};
|
|
|
|
return (
|
|
<Screen>
|
|
<AppHeader
|
|
title={t("common.branches")}
|
|
subtitle={`${branches.length} locations across Addis Ababa`}
|
|
/>
|
|
<ScrollView
|
|
contentContainerStyle={{
|
|
paddingHorizontal: gutter,
|
|
paddingBottom: 40,
|
|
gap: 12,
|
|
}}
|
|
>
|
|
<Button
|
|
title={t("common.useLocation")}
|
|
icon="navigate-outline"
|
|
onPress={useMyLocation}
|
|
/>
|
|
<Text variant="h3">{t("common.nearestBranch")}</Text>
|
|
|
|
{sorted.map((b, index) => {
|
|
const selected = branchId === b.id;
|
|
const km = coords ? dist(coords, b) : null;
|
|
return (
|
|
<Card
|
|
key={b.id}
|
|
style={{
|
|
borderWidth: selected ? 2 : 1,
|
|
borderColor: selected ? colors.brand : colors.line,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 10 }}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text variant="h3">{b.name}</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
|
|
{b.area}
|
|
</Text>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
|
|
{b.hours}
|
|
{b.pickupAvailable ? " · Pickup" : ""}
|
|
</Text>
|
|
</View>
|
|
<View style={{ alignItems: "flex-end", gap: 6 }}>
|
|
{selected ? <Badge label="Selected" tone="brand" /> : null}
|
|
{index === 0 && coords ? <Badge label="Nearest" tone="info" /> : null}
|
|
{km != null ? (
|
|
<Text variant="label" color={colors.brandText}>
|
|
{km.toFixed(1)} km
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, marginTop: 14 }}>
|
|
<Button
|
|
title="Call"
|
|
size="md"
|
|
fullWidth={false}
|
|
variant="outline"
|
|
icon="call-outline"
|
|
onPress={() => Linking.openURL(`tel:${b.phone}`)}
|
|
/>
|
|
<Button
|
|
title="Maps"
|
|
size="md"
|
|
fullWidth={false}
|
|
variant="outline"
|
|
icon="map-outline"
|
|
onPress={() => Linking.openURL(b.mapsUrl)}
|
|
/>
|
|
<Button
|
|
title="Set pickup"
|
|
size="md"
|
|
fullWidth={false}
|
|
onPress={() => {
|
|
setBranchId(b.id);
|
|
setFulfillment("pickup");
|
|
Alert.alert("Pickup set", `${b.name} selected for checkout.`);
|
|
}}
|
|
/>
|
|
</View>
|
|
</Card>
|
|
);
|
|
})}
|
|
|
|
<Button
|
|
title="Go to checkout"
|
|
variant="ghost"
|
|
onPress={() => router.push(routes.checkout)}
|
|
/>
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|