This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/app/(root)/(tabs)/cart.tsx
kirukib 7310a349c5 Add Gishen mobile app with complete demo shopping and care flows.
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>
2026-08-07 01:21:32 +03:00

184 lines
6.7 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import {
Alert,
Image,
Pressable,
ScrollView,
TextInput,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/Button";
import { Card, InfoRow } from "@/components/ui/Card";
import { EmptyState } from "@/components/ui/EmptyState";
import { QtyStepper } from "@/components/ui/QtyStepper";
import { Screen, TAB_BAR_INSET } from "@/components/ui/Screen";
import { StickyBar } from "@/components/ui/StickyBar";
import { Text } from "@/components/ui/Text";
import { DELIVERY_FEE, DEMO_VOUCHER, voucherDiscount } from "@/lib/commerce";
import { routes } from "@/lib/routes";
import { useCartStore } from "@/lib/stores/cartStore";
import { colors, fonts, gutter, radii } from "@/lib/theme";
import { formatETB } from "@/lib/utils";
export default function CartScreen() {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const items = useCartStore((s) => s.items);
const setQuantity = useCartStore((s) => s.setQuantity);
const removeItem = useCartStore((s) => s.removeItem);
const subtotal = useCartStore((s) => s.subtotal());
const voucher = useCartStore((s) => s.voucherCode);
const setVoucher = useCartStore((s) => s.setVoucher);
const delivery = items.length ? DELIVERY_FEE : 0;
const discount = voucherDiscount(subtotal, voucher);
const total = subtotal + delivery - discount;
return (
<Screen>
<ScrollView
contentContainerStyle={{
paddingTop: insets.top + 16,
paddingBottom: TAB_BAR_INSET + 100,
paddingHorizontal: gutter,
}}
>
<Text variant="h1">{t("common.cart")}</Text>
<Text variant="bodySm" color={colors.subtle} style={{ marginTop: 4 }}>
{items.length} item{items.length === 1 ? "" : "s"} in your bag
</Text>
{items.length === 0 ? (
<EmptyState
icon="bag-handle-outline"
title={t("common.emptyCart")}
body="Browse the shop and add medicines or wellness products."
actionTitle="Explore products"
onAction={() => router.push(routes.shop)}
/>
) : (
<View style={{ marginTop: 16, gap: 12 }}>
{items.map((item) => (
<Card key={item.productId} elevated>
<View style={{ flexDirection: "row", gap: 12 }}>
<Image
source={{ uri: item.image }}
style={{
width: 72,
height: 72,
borderRadius: radii.sm,
backgroundColor: colors.surfaceAlt,
}}
resizeMode="contain"
/>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<Text variant="h3" style={{ flex: 1 }} numberOfLines={2}>
{item.name}
</Text>
<Pressable onPress={() => removeItem(item.productId)} hitSlop={8}>
<Ionicons name="trash-outline" size={18} color={colors.subtle} />
</Pressable>
</View>
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
{item.unit}
{item.requiresPrescription ? " · RX" : ""}
</Text>
<View
style={{
marginTop: 10,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text variant="price">{formatETB(item.price)}</Text>
<QtyStepper
size="sm"
value={item.quantity}
onChange={(n) => setQuantity(item.productId, n)}
min={0}
/>
</View>
</View>
</View>
</Card>
))}
<Pressable onPress={() => router.push(routes.shop)}>
<Text variant="label" color={colors.brandText}>
+ Add product
</Text>
</Pressable>
<Card elevated={false} style={{ marginTop: 8 }}>
<Text variant="h3" style={{ marginBottom: 8 }}>
Voucher
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderColor: colors.line,
borderRadius: radii.pill,
paddingLeft: 14,
paddingRight: 6,
height: 48,
}}
>
<TextInput
value={voucher ?? ""}
onChangeText={(v) => setVoucher(v || null)}
placeholder="Try GISHEN10"
placeholderTextColor={colors.subtle}
style={{
flex: 1,
fontFamily: fonts.regular,
fontSize: 14,
color: colors.ink,
}}
/>
<Button
title="Apply"
size="sm"
fullWidth={false}
onPress={() => {
if ((voucher ?? "").toUpperCase() === DEMO_VOUCHER) {
setVoucher(DEMO_VOUCHER);
Alert.alert("Applied", "10% off your basket.");
} else {
Alert.alert("Invalid code", `Use ${DEMO_VOUCHER} for a demo discount.`);
}
}}
/>
</View>
</Card>
<Card elevated={false}>
<InfoRow label="Payment" value={formatETB(subtotal)} />
<InfoRow label="Delivery" value={formatETB(delivery)} />
{discount > 0 ? (
<InfoRow label="Discount" value={`- ${formatETB(discount)}`} valueColor={colors.brandText} />
) : null}
<InfoRow label={t("common.total")} value={formatETB(total)} emphasis last />
</Card>
</View>
)}
</ScrollView>
{items.length > 0 ? (
<StickyBar>
<Button
title={t("common.payNow")}
iconRight="arrow-forward"
onPress={() => router.push(routes.checkout)}
/>
</StickyBar>
) : null}
</Screen>
);
}