import type { StorefrontApi } from "@shopify/hydrogen"; import Link from "next/link"; import type { PRODUCT_CARD_QUERY } from "@/lib/fragments"; import { shopifyImageUrl, srcSetFor } from "@/lib/image"; import { formatPrice } from "@/lib/money"; /** The typed product card node consumed by grids across the storefront. */ export type ProductCardData = NonNullable< StorefrontApi.ResultOf["product"] >; type ProductCardProps = { product: ProductCardData; /** Eager-load the first row on hero-less pages (PLP/search LCP). */ loading?: "eager" | "lazy"; fetchPriority?: "high" | "low" | "auto"; }; /** * Shared `ProductCard` — image with `images[1]` hover-swap, title * (`type-body-sm line-clamp-2`), price block (`text-sale` + * `` on sale), and `badge-sale`/`badge-soldout` * positioned `absolute start-2 top-2`. Reused by home, collection, search, and * the PDP "you may also like" strip (engineering.md F13). * * The whole card is a stretched link via `.card-link::after` so the title * anchor's hit area climbs to the whole card. Server-rendered (no client * interactivity). */ export function ProductCard({ product, loading = "lazy", fetchPriority = "auto", }: ProductCardProps) { const primaryImage = product.featuredImage; const secondaryImage = product.images.nodes[1] ?? null; const minPrice = product.priceRange.minVariantPrice; const compareAt = product.compareAtPriceRange?.minVariantPrice ?? null; const onSale = compareAt && Number(compareAt.amount) > Number(minPrice.amount); const soldOut = !product.availableForSale; return (
{primaryImage ? (
{primaryImage.altText
) : (
)} {secondaryImage ? (
{secondaryImage.altText
) : null} {onSale ? ( Sale ) : null} {soldOut ? ( Sold out ) : null}

{product.title}

{onSale ? "Sale price: " : "Price: "} {formatPrice(minPrice)} {onSale && compareAt ? ( Regular price: {formatPrice(compareAt)} ) : null}
); }