import * as React from 'react' import {type Product} from '@shopify/shop-minis-platform' import {cva, type VariantProps} from 'class-variance-authority' import {Slot as SlotPrimitive} from 'radix-ui' import {useShopNavigation} from '../../hooks/navigation/useShopNavigation' import {useSavedProductsActions} from '../../hooks/user/useSavedProductsActions' import {ProductReviewStars} from '../../internal/components/product-review-stars' import {useProductImpression} from '../../internal/useProductImpression' import {cn} from '../../lib/utils' import {formatMoney} from '../../utils/formatMoney' import {Touchable} from '../atoms/touchable' import {Card, CardContent, CardAction} from '../ui/card' import {FavoriteButton} from './favorite-button' const productLinkVariants = cva('', { variants: { layout: { horizontal: 'w-full !flex-row items-center gap-3 px-4 py-3', vertical: 'flex-col', }, discount: { none: '', small: '', large: '', }, }, defaultVariants: { layout: 'horizontal', discount: 'none', }, }) // Primitive components (building blocks) export interface ProductLinkRootProps extends React.ComponentProps, VariantProps { layout?: 'horizontal' | 'vertical' asChild?: boolean onPress?: () => void } function ProductLinkRoot({ className, layout, discount, asChild = false, onPress, ...props }: ProductLinkRootProps) { const Comp = asChild ? SlotPrimitive.Root : Card return ( ) } function ProductLinkImage({ className, layout = 'horizontal', ...props }: React.ComponentProps<'div'> & {layout?: 'horizontal' | 'vertical'}) { return (
) } function ProductLinkInfo({ className, layout = 'horizontal', ...props }: React.ComponentProps & { layout?: 'horizontal' | 'vertical' }) { return ( ) } function ProductLinkTitle({ className, children, ...props }: React.ComponentProps<'h3'>) { return (

{children}

) } function ProductLinkPrice({className, ...props}: React.ComponentProps<'div'>) { return (
) } function ProductLinkCurrentPrice({ className, ...props }: React.ComponentProps<'span'>) { return ( ) } function ProductLinkOriginalPrice({ className, ...props }: React.ComponentProps<'span'>) { return ( ) } function ProductLinkDiscountPrice({ className, ...props }: React.ComponentProps<'span'>) { return ( ) } function ProductLinkRating({className, ...props}: React.ComponentProps<'div'>) { return (
) } function ProductLinkActions({ className, hideFavoriteAction = false, onPress, filled = false, customAction, ...props }: React.ComponentProps & { hideFavoriteAction?: boolean onPress?: () => void filled?: boolean customAction?: React.ReactNode }) { const favoriteAction = hideFavoriteAction ? null : ( ) return ( {customAction ? <>{customAction} : favoriteAction} ) } /** * A list item component for displaying products in search results, lists, and feeds. `ProductLink` works well for list views and search results where vertical space is limited. For grid layouts with larger images, you can use `ProductCard` instead. Supports custom actions (e.g., "Add to Cart") in place of the favorite button. * @publicDocs */ export interface ProductLinkDocProps { /** The product to display */ product: Product /** Hide the favorite/save button */ hideFavoriteAction?: boolean /** Callback when the product link is clicked */ onClick?: (product: Product) => void /** Hide the review stars */ reviewsDisabled?: boolean /** Custom action element to replace the favorite button. Must be provided with `onCustomActionClick`. */ customAction?: React.ReactNode /** Callback when the custom action is clicked. Must be provided with `customAction`. */ onCustomActionClick?: () => void /** Whether to disable impression tracking */ impressionTrackingDisabled?: boolean } export type ProductLinkProps = { product: Product hideFavoriteAction?: boolean onClick?: (product: Product) => void reviewsDisabled?: boolean impressionTrackingDisabled?: boolean } & ( | { customAction?: never onCustomActionClick?: never } | { customAction: React.ReactNode onCustomActionClick: () => void } ) // Composed ProductLink component function ProductLink({ product, hideFavoriteAction = false, onClick, customAction, onCustomActionClick, reviewsDisabled = false, impressionTrackingDisabled = false, }: ProductLinkProps) { const {navigateToProduct} = useShopNavigation() const {saveProduct, unsaveProduct} = useSavedProductsActions() const {ref: impressionRef} = useProductImpression({ productId: product.id, skip: impressionTrackingDisabled, }) const { id, title, featuredImage, reviewAnalytics, price, compareAtPrice, isFavorited, selectedVariant, defaultVariantId, shop, } = product // Local state for optimistic UI updates const [isFavoritedLocal, setIsFavoritedLocal] = React.useState(isFavorited) const averageRating = reviewAnalytics?.averageRating const reviewCount = reviewAnalytics?.reviewCount const amount = price?.amount ? formatMoney(price?.amount, price?.currencyCode) : undefined const imageUrl = featuredImage?.url const imageAltText = featuredImage?.altText || title const compareAtPriceAmount = compareAtPrice?.amount ? formatMoney(compareAtPrice?.amount, compareAtPrice?.currencyCode) : undefined const hasDiscount = compareAtPriceAmount && compareAtPriceAmount !== amount const handlePress = React.useCallback(() => { navigateToProduct({ productId: id, }) onClick?.(product) }, [navigateToProduct, id, onClick, product]) const handleActionPress = React.useCallback(async () => { if (customAction || onCustomActionClick) { onCustomActionClick?.() return } const previousState = isFavoritedLocal // Optimistic update setIsFavoritedLocal(!previousState) try { if (previousState) { await unsaveProduct({ productId: id, shopId: shop.id, productVariantId: selectedVariant?.id || defaultVariantId, }) } else { await saveProduct({ productId: id, shopId: shop.id, productVariantId: selectedVariant?.id || defaultVariantId, }) } } catch (error) { // Revert optimistic update on error setIsFavoritedLocal(previousState) } }, [ customAction, onCustomActionClick, isFavoritedLocal, unsaveProduct, id, shop.id, selectedVariant?.id, defaultVariantId, saveProduct, ]) return (
{imageUrl ? ( {imageAltText} ) : (
No Image
)}
{title} {averageRating && !reviewsDisabled ? ( ) : null} {hasDiscount ? ( <> {amount} {compareAtPriceAmount} ) : ( {amount} )}
) } export {ProductLink}