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)/(screens)/product/[id].tsx
kirukib 5391780b9b Ship Rx Care tab, notifications inbox, and guest pickup codes.
Replace the profile tab with prescription management (inline reminders and family invite), add a shared tab top bar, separate notifications from orders, and support any-branch guest checkout with GSH codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:35:51 +03:00

445 lines
16 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import { useMemo, useState } from "react";
import {
Image,
Pressable,
ScrollView,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { Badge, Price, Rating, StockLabel } from "@/components/ui/Badge";
import { Button, IconButton } from "@/components/ui/Button";
import { Card, InfoRow, Section } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
import { QtyStepper } from "@/components/ui/QtyStepper";
import { StickyBar } from "@/components/ui/StickyBar";
import { Text } from "@/components/ui/Text";
import { branches } from "@/lib/data/branches";
import { getDrugFacts, getReviewBreakdown } from "@/lib/data/clinical";
import { products } from "@/lib/data/products";
import { useCartStore } from "@/lib/stores/cartStore";
import { useFavoritesStore } from "@/lib/stores/favoritesStore";
import { colors, gutter, radii, space } from "@/lib/theme";
import { formatETB } from "@/lib/utils";
import { routes } from "@/lib/routes";
export default function ProductDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const product = products.find((p) => p.id === id) ?? products[0];
const facts = useMemo(() => getDrugFacts(product), [product]);
const reviews = useMemo(() => getReviewBreakdown(product), [product]);
const [qty, setQty] = useState(1);
const [dose, setDose] = useState(facts.dosages[0] ?? "");
const [tab, setTab] = useState<"details" | "pharmacy">("details");
const [more, setMore] = useState(false);
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const addProduct = useCartStore((s) => s.addProduct);
const toggle = useFavoritesStore((s) => s.toggle);
const liked = useFavoritesStore((s) => s.has(product.id));
const addToCart = () => {
addProduct(product, qty);
};
return (
<View style={{ flex: 1, backgroundColor: colors.page }}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 120 }}
>
<View
style={{
backgroundColor: colors.surfaceAlt,
paddingTop: insets.top + 8,
paddingBottom: 28,
}}
>
<View
style={{
paddingHorizontal: gutter,
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 8,
}}
>
<IconButton
icon="arrow-back"
accessibilityLabel="Go back"
onPress={() => router.back()}
/>
<IconButton
icon={liked ? "heart" : "heart-outline"}
onPress={() => toggle(product.id)}
color={liked ? colors.danger : colors.ink}
accessibilityLabel={liked ? "Remove favorite" : "Add favorite"}
/>
</View>
<Image
source={{ uri: product.image }}
style={{ width: "100%", height: 240 }}
resizeMode="contain"
/>
</View>
<View
style={{
marginTop: -24,
marginHorizontal: gutter,
backgroundColor: colors.surface,
borderRadius: radii.xl,
borderWidth: 1,
borderColor: colors.line,
padding: space.xl,
gap: 14,
}}
>
{product.requiresPrescription ? (
<Badge label="RX needed" tone="info" icon="document-text-outline" />
) : (
<Badge label={facts.useCase} tone="neutral" />
)}
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 12 }}>
<View style={{ flex: 1 }}>
<Text variant="h1">{product.name}</Text>
<Text variant="caption" color={colors.subtle} style={{ marginTop: 4 }}>
{product.brand} · {product.unit}
</Text>
</View>
<Rating value={product.rating} count={product.reviews} showStars={false} />
</View>
<Price
value={product.price}
compareAt={product.compareAt}
size="lg"
format={formatETB}
/>
{facts.dosages.length > 0 ? (
<View>
<Text variant="label" color={colors.muted} style={{ marginBottom: 8 }}>
Strength
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
{facts.dosages.map((d) => (
<Chip
key={d}
label={d}
selected={dose === d}
onPress={() => setDose(d)}
/>
))}
</View>
</View>
) : null}
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<QtyStepper value={qty} onChange={setQty} />
<StockLabel inStock={product.inStock} count={facts.stockCount} />
</View>
<View style={{ gap: 8 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Ionicons name="bicycle-outline" size={16} color={colors.info} />
<Text variant="bodySm" color={colors.muted}>
Same-day delivery in Addis Ababa
</Text>
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Ionicons name="shield-checkmark-outline" size={16} color={colors.brandText} />
<Text variant="bodySm" color={colors.muted}>
Pharmacist-verified · 30-day return policy
</Text>
</View>
</View>
<Button
title={t("common.addProduct")}
icon="bag-add-outline"
onPress={() => {
addToCart();
router.push(routes.cart);
}}
disabled={!product.inStock}
/>
</View>
<View style={{ paddingHorizontal: gutter, marginTop: 18 }}>
<View
style={{
flexDirection: "row",
backgroundColor: colors.surfaceAlt,
borderRadius: radii.pill,
padding: 4,
}}
>
{(["details", "pharmacy"] as const).map((key) => (
<Pressable
key={key}
onPress={() => setTab(key)}
style={{
flex: 1,
alignItems: "center",
paddingVertical: 10,
borderRadius: radii.pill,
backgroundColor: tab === key ? colors.surface : "transparent",
}}
>
<Text variant="h3" color={tab === key ? colors.ink : colors.subtle}>
{key === "details" ? "Details" : "Pharmacy"}
</Text>
</Pressable>
))}
</View>
</View>
{tab === "details" ? (
<View style={{ paddingHorizontal: gutter, paddingBottom: 20 }}>
<Section title="Description">
<Text variant="body" color={colors.muted}>
{more ? product.description : `${product.description.slice(0, 140)}…`}
</Text>
<Pressable onPress={() => setMore(!more)} style={{ marginTop: 6 }}>
<Text variant="label" color={colors.brandText}>
{more ? "Show less" : "Read more"}
</Text>
</Pressable>
</Section>
<Section title="Drug facts">
<Card elevated={false}>
<InfoRow label="Generic name" value={facts.genericName} />
<InfoRow label="Brand names" value={facts.brandNames} />
<InfoRow label="Drug class" value={facts.drugClass} last />
</Card>
</Section>
<Section title="Side effects">
<Text variant="caption" color={colors.subtle} style={{ marginBottom: 8 }}>
Common (mild)
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
{facts.sideEffects.mild.map((s) => (
<Badge key={s} label={s} tone="neutral" icon="remove-outline" />
))}
</View>
{facts.sideEffects.severe.length > 0 ? (
<>
<Text
variant="caption"
color={colors.subtle}
style={{ marginTop: 14, marginBottom: 8 }}
>
Severe (rare)
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
{facts.sideEffects.severe.map((s) => (
<Badge key={s} label={s} tone="danger" icon="alert-outline" />
))}
</View>
</>
) : null}
</Section>
<Section title="Storage">
<View style={{ gap: 10 }}>
{facts.storage.map((note) => (
<Card key={note.title} elevated={false}>
<View style={{ flexDirection: "row", gap: 12, alignItems: "flex-start" }}>
<View
style={{
width: 36,
height: 36,
borderRadius: 12,
backgroundColor: colors.brandSoft,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name={note.icon} size={18} color={colors.brandText} />
</View>
<View style={{ flex: 1 }}>
<Text variant="h3">{note.title}</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 2 }}>
{note.body}
</Text>
</View>
</View>
</Card>
))}
</View>
</Section>
<Section title="Directions of use">
<View style={{ gap: 0, paddingLeft: 8 }}>
{facts.directions.map((step, index) => (
<View key={step.title} style={{ flexDirection: "row", gap: 14 }}>
<View style={{ alignItems: "center", width: 22 }}>
<View
style={{
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: colors.brand,
alignItems: "center",
justifyContent: "center",
}}
>
<Text variant="micro" color={colors.ink}>
{index + 1}
</Text>
</View>
{index < facts.directions.length - 1 ? (
<View
style={{
width: 2,
flex: 1,
minHeight: 28,
backgroundColor: colors.brandBorder,
}}
/>
) : null}
</View>
<View style={{ flex: 1, paddingBottom: 16 }}>
<Text variant="h3">{step.title}</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 2 }}>
{step.body}
</Text>
</View>
</View>
))}
</View>
</Section>
<Section title="Reviews">
<Card elevated={false}>
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 8 }}>
<Text variant="display">{product.rating.toFixed(1)}</Text>
<Text variant="bodySm" color={colors.subtle} style={{ marginBottom: 6 }}>
{product.reviews} reviews
</Text>
</View>
<View style={{ marginTop: 14, gap: 8 }}>
{reviews.map((row) => {
const max = reviews[0]?.count || 1;
return (
<View
key={row.stars}
style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
>
<Text variant="caption" color={colors.muted} style={{ width: 14 }}>
{row.stars}
</Text>
<View
style={{
flex: 1,
height: 6,
borderRadius: 3,
backgroundColor: colors.surfaceAlt,
overflow: "hidden",
}}
>
<View
style={{
width: `${Math.round((row.count / max) * 100)}%`,
height: "100%",
backgroundColor: colors.brand,
}}
/>
</View>
</View>
);
})}
</View>
</Card>
</Section>
<Card tone="brand" elevated={false} style={{ marginTop: 8 }}>
<Text variant="h3">Pharmacist tip</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
{facts.advice}
</Text>
</Card>
</View>
) : (
<View style={{ paddingHorizontal: gutter, paddingBottom: 20 }}>
<Section title="Available at">
{branches.slice(0, 4).map((branch, index) => (
<Card
key={branch.id}
onPress={() => router.push(routes.branches)}
style={{ marginTop: index === 0 ? 0 : 10 }}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
gap: 12,
}}
>
<View style={{ flex: 1 }}>
<Text variant="h3">{branch.name}</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
{branch.area}
</Text>
<Text variant="caption" color={colors.subtle} style={{ marginTop: 4 }}>
{branch.hours}
{branch.pickupAvailable ? " · Pickup available" : ""}
</Text>
</View>
{index === 0 ? (
<StockLabel inStock={product.inStock} count={facts.stockCount} />
) : (
<Badge
label={product.inStock ? "In stock" : "Call"}
tone={product.inStock ? "brand" : "warn"}
/>
)}
</View>
</Card>
))}
<Button
title="View all branches"
variant="outline"
onPress={() => router.push(routes.branches)}
style={{ marginTop: 14 }}
/>
</Section>
</View>
)}
</ScrollView>
<StickyBar floating>
<View style={{ flex: 1 }}>
<Text variant="caption" color="rgba(255,255,255,0.7)" numberOfLines={1}>
{product.name}
</Text>
<Text variant="h3" color={colors.surface}>
{formatETB(product.price * qty)}
</Text>
</View>
<Button
title="Add"
size="md"
fullWidth={false}
icon="bag-add-outline"
onPress={addToCart}
disabled={!product.inStock}
style={{ minWidth: 110 }}
/>
</StickyBar>
</View>
);
}