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 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

440 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="chevron-back" onPress={() => router.back()} />
<IconButton
icon={liked ? "heart" : "heart-outline"}
onPress={() => toggle(product.id)}
color={liked ? colors.danger : colors.ink}
/>
</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>
);
}