146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import type { GestureResponderEvent } from "react-native";
|
|
import type { JSX } from "react";
|
|
import { useState } from "react";
|
|
import { Pressable, StyleSheet, View } from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
import { Button, Separator, Surface, Typography } from "heroui-native";
|
|
import Svg, { Defs, RadialGradient, Rect, Stop } from "react-native-svg";
|
|
import type { Product } from "@/data/mock";
|
|
import { availabilityInfo } from "@/lib/status";
|
|
import { StatusChip } from "@/components/StatusChip";
|
|
import { MonoText } from "@/components/MonoText";
|
|
import { useModals } from "@/state/modals";
|
|
|
|
interface ListingCardProps {
|
|
product: Product;
|
|
variant?: "list" | "grid";
|
|
}
|
|
|
|
function CardHighlightOverlay({ gradientId }: { gradientId: string }): JSX.Element {
|
|
return (
|
|
<Svg style={StyleSheet.absoluteFill} width="100%" height="100%" pointerEvents="none">
|
|
<Defs>
|
|
<RadialGradient id={gradientId} cx="30%" cy="25%" r="60%">
|
|
<Stop offset="0%" stopColor="#ffffff" stopOpacity={0.07} />
|
|
<Stop offset="100%" stopColor="#ffffff" stopOpacity={0} />
|
|
</RadialGradient>
|
|
</Defs>
|
|
<Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
|
</Svg>
|
|
);
|
|
}
|
|
|
|
export function ListingCard({ product, variant = "grid" }: ListingCardProps): JSX.Element {
|
|
const router = useRouter();
|
|
const { requestBuy } = useModals();
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const availability = availabilityInfo(product.qty);
|
|
const isOut = product.qty === 0;
|
|
|
|
const openListing = () => router.push(`/listing/${product.id}`);
|
|
|
|
const handleBuyPress = (event: GestureResponderEvent) => {
|
|
event.stopPropagation();
|
|
requestBuy(product.id);
|
|
};
|
|
|
|
if (variant === "list") {
|
|
return (
|
|
<Pressable
|
|
onPress={openListing}
|
|
onHoverIn={() => setIsHovered(true)}
|
|
onHoverOut={() => setIsHovered(false)}
|
|
>
|
|
<Surface
|
|
variant="default"
|
|
className={`flex-row items-stretch gap-0 overflow-hidden rounded-xl border p-0 ${
|
|
isHovered ? "border-border-secondary" : "border-border"
|
|
}`}
|
|
>
|
|
<View
|
|
className="w-24 items-center justify-center md:w-36"
|
|
style={{ backgroundColor: product.bg }}
|
|
>
|
|
<Typography className="text-4xl">{product.emoji}</Typography>
|
|
</View>
|
|
<Separator orientation="vertical" />
|
|
<View className="w-24 justify-center gap-0.5 p-3 md:w-32">
|
|
<Typography type="h6" className="text-accent">
|
|
{product.priceNum}
|
|
</Typography>
|
|
<Typography type="body-xs" color="muted">
|
|
{product.cur}
|
|
</Typography>
|
|
</View>
|
|
<Separator orientation="vertical" />
|
|
<View className="flex-1 justify-center gap-1 p-3">
|
|
<Typography type="body-sm" weight="semibold" numberOfLines={1}>
|
|
{product.name}
|
|
</Typography>
|
|
<Typography type="body-xs" color="muted" numberOfLines={2}>
|
|
{product.desc}
|
|
</Typography>
|
|
</View>
|
|
<Separator orientation="vertical" />
|
|
<View className="w-28 items-stretch justify-center gap-2 p-3">
|
|
<StatusChip label={availability.label} color={availability.color} />
|
|
<Button
|
|
size="sm"
|
|
variant={isOut ? "secondary" : "primary"}
|
|
isDisabled={isOut}
|
|
onPress={handleBuyPress}
|
|
>
|
|
<Button.Label>{isOut ? "Out of Stock" : "Buy Now"}</Button.Label>
|
|
</Button>
|
|
</View>
|
|
</Surface>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Pressable
|
|
onPress={openListing}
|
|
onHoverIn={() => setIsHovered(true)}
|
|
onHoverOut={() => setIsHovered(false)}
|
|
className="w-full md:w-[calc(50%-10px)] lg:w-[calc(25%-18px)]"
|
|
>
|
|
<Surface
|
|
variant="default"
|
|
className={`gap-0 overflow-hidden rounded-xl border p-0 ${isHovered ? "border-border-secondary" : "border-border"}`}
|
|
>
|
|
<View
|
|
className="aspect-[4/3] items-center justify-center"
|
|
style={{ backgroundColor: product.bg }}
|
|
>
|
|
<CardHighlightOverlay gradientId={`card-highlight-${product.id}`} />
|
|
<Typography className="text-6xl lg:text-7xl">{product.emoji}</Typography>
|
|
</View>
|
|
<View className="gap-1 p-2.5">
|
|
<View className="flex-row items-start justify-between gap-2">
|
|
<Typography type="body-sm" weight="semibold" numberOfLines={2} className="flex-1">
|
|
{product.name}
|
|
</Typography>
|
|
<StatusChip label={availability.label} color={availability.color} />
|
|
</View>
|
|
<MonoText type="body-xs" color="muted" numberOfLines={1}>
|
|
{product.seller}
|
|
</MonoText>
|
|
<Typography type="body" weight="bold" className="mt-1 text-accent">
|
|
{product.price}
|
|
</Typography>
|
|
<Button
|
|
size="sm"
|
|
variant={isOut ? "secondary" : "primary"}
|
|
isDisabled={isOut}
|
|
className="mt-2 w-full"
|
|
onPress={handleBuyPress}
|
|
>
|
|
<Button.Label>{isOut ? "Out of Stock" : "Buy Now"}</Button.Label>
|
|
</Button>
|
|
</View>
|
|
</Surface>
|
|
</Pressable>
|
|
);
|
|
}
|