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/components/ui/ProductCard.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

263 lines
7.6 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { Image, Pressable, View } from "react-native";
import { AddButton } from "@/components/ui/Button";
import { Badge, Price, StockLabel } from "@/components/ui/Badge";
import { MetaTag } from "@/components/ui/Chip";
import { Text } from "@/components/ui/Text";
import { getDrugFacts } from "@/lib/data/clinical";
import type { Product } from "@/lib/data/products";
import { routes } from "@/lib/routes";
import { useCartStore } from "@/lib/stores/cartStore";
import { useFavoritesStore } from "@/lib/stores/favoritesStore";
import { colors, radii, shadows, space } from "@/lib/theme";
import { formatETB } from "@/lib/utils";
export const GRID_CARD_GAP = 12;
function discount(product: Product) {
if (!product.compareAt || product.compareAt <= product.price) return null;
return `${Math.round(((product.compareAt - product.price) / product.compareAt) * 100)}% OFF`;
}
function useCardActions(product: Product) {
const addProduct = useCartStore((s) => s.addProduct);
return {
open: () => router.push(routes.product(product.id)),
add: () => addProduct(product, 1),
};
}
/** Grid tile used in shop, search results and category listings. */
export function ProductCard({
product,
width,
}: {
product: Product;
width?: number | `${number}%`;
}) {
const { open, add } = useCardActions(product);
const facts = getDrugFacts(product);
const off = discount(product);
return (
<Pressable
onPress={open}
style={({ pressed }) => [
{
width,
backgroundColor: colors.surface,
borderRadius: radii.card,
borderWidth: 1,
borderColor: colors.line,
padding: space.md,
opacity: pressed ? 0.94 : 1,
},
shadows.xs,
]}
>
<View
style={{
height: 108,
borderRadius: radii.sm,
backgroundColor: colors.surfaceAlt,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}}
>
<Image
source={{ uri: product.image }}
style={{ width: "82%", height: "82%" }}
resizeMode="contain"
/>
{off ? (
<Badge label={off} solid style={{ position: "absolute", top: 8, left: 8 }} />
) : null}
{product.requiresPrescription ? (
<Badge
label="RX"
tone="info"
style={{ position: "absolute", top: 8, right: 8 }}
/>
) : null}
</View>
<Text variant="micro" color={colors.subtle} style={{ marginTop: 10 }}>
{facts.useCase}
</Text>
<Text variant="h3" numberOfLines={2} style={{ marginTop: 2, minHeight: 40 }}>
{product.name}
</Text>
<View style={{ marginTop: 6 }}>
<Price value={product.price} compareAt={product.compareAt} format={formatETB} />
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10, marginTop: 8 }}>
<MetaTag label={facts.form} icon="ellipse-outline" />
<MetaTag label={product.unit} />
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: 10,
}}
>
<StockLabel inStock={product.inStock} count={facts.stockCount} />
<AddButton onPress={add} disabled={!product.inStock} />
</View>
</Pressable>
);
}
/** Horizontal row used in "Most popular", favorites and list-view results. */
export function ProductRow({ product }: { product: Product }) {
const { open, add } = useCardActions(product);
const facts = getDrugFacts(product);
return (
<Pressable
onPress={open}
style={({ pressed }) => [
{
flexDirection: "row",
gap: space.md,
backgroundColor: colors.surface,
borderRadius: radii.card,
borderWidth: 1,
borderColor: colors.line,
padding: space.md,
alignItems: "center",
opacity: pressed ? 0.94 : 1,
},
shadows.xs,
]}
>
<View
style={{
width: 66,
height: 66,
borderRadius: radii.sm,
backgroundColor: colors.surfaceAlt,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}}
>
<Image
source={{ uri: product.image }}
style={{ width: "80%", height: "80%" }}
resizeMode="contain"
/>
</View>
<View style={{ flex: 1 }}>
<Text variant="micro" color={colors.subtle}>
{facts.useCase}
</Text>
<Text variant="h3" numberOfLines={1} style={{ marginTop: 1 }}>
{product.name}
</Text>
<View style={{ marginTop: 4 }}>
<Price value={product.price} compareAt={product.compareAt} format={formatETB} />
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10, marginTop: 5 }}>
<MetaTag label={facts.form} icon="ellipse-outline" />
<StockLabel inStock={product.inStock} count={facts.stockCount} />
</View>
</View>
<AddButton onPress={add} disabled={!product.inStock} />
</Pressable>
);
}
/** Compact tile for horizontal carousels on the home screen. */
export function ProductTile({ product }: { product: Product }) {
const { open, add } = useCardActions(product);
const liked = useFavoritesStore((s) => s.has(product.id));
const toggle = useFavoritesStore((s) => s.toggle);
const off = discount(product);
return (
<Pressable
onPress={open}
style={({ pressed }) => [
{
width: 158,
backgroundColor: colors.surface,
borderRadius: radii.card,
borderWidth: 1,
borderColor: colors.line,
padding: space.md,
opacity: pressed ? 0.94 : 1,
},
shadows.xs,
]}
>
<View
style={{
height: 96,
borderRadius: radii.sm,
backgroundColor: colors.surfaceAlt,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}}
>
<Image
source={{ uri: product.image }}
style={{ width: "82%", height: "82%" }}
resizeMode="contain"
/>
{off ? (
<Badge label={off} solid style={{ position: "absolute", top: 6, left: 6 }} />
) : null}
<Pressable
onPress={() => toggle(product.id)}
hitSlop={8}
style={{
position: "absolute",
top: 6,
right: 6,
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: colors.surface,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons
name={liked ? "heart" : "heart-outline"}
size={14}
color={liked ? colors.danger : colors.muted}
/>
</Pressable>
</View>
<Text variant="h3" numberOfLines={2} style={{ marginTop: 10, minHeight: 40 }}>
{product.name}
</Text>
<View style={{ marginTop: 4 }}>
<Price value={product.price} compareAt={product.compareAt} size="sm" format={formatETB} />
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: 8,
}}
>
<StockLabel inStock={product.inStock} />
<AddButton onPress={add} disabled={!product.inStock} size={30} />
</View>
</Pressable>
);
}