ProductCard
The product tile of every grid: image, badge, name, price, rating, wishlist heart and a real Add to cart button, in five layouts.
import { ProductCard, ProductCardProps, ProductCardLayout } from '@salla.sa/twilight-theme-engine/components/product';In plain words
A component is a function that returns a piece of page. ProductCard is the one that draws a single product: give it one product object, the kind the Salla API returns, and it draws the picture, the name, the price and the buttons.
The things you pass to a component are its props, written like HTML attributes: <ProductCard product={item} layout="horizontal" />. layout picks one of five looks, and a few on/off props add a shadow, a stock badge, or hide the Add to cart button.
The buttons are live. Add to cart talks to Salla and adds the product to the shopper's cart; the heart toggles the wishlist.
Signature
const ProductCard: React.MemoExoticComponent<(props: ProductCardProps) => JSX.Element>
type ProductCardLayout = 'vertical' | 'horizontal' | 'fullImage' | 'minimal' | 'special';
interface ProductCardProps {
product: Product;
layout?: ProductCardLayout; // 'vertical'
className?: string;
withShadow?: boolean; // false
withQuantity?: boolean; // false: "Remained N" / "Out of Stock" badge
withoutAddButton?: boolean; // false
index?: number; // 0: animation delay of index × 100ms
imagePriority?: boolean; // false: eager image for the first cards
sizes?: string; // the image sizes hint
}Try it live
Loading products…
import { useQuery } from '@tanstack/react-query';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
import { ProductCard } from '@salla.sa/twilight-theme-engine/components/product';
export function LatestProducts() {
const { data } = useQuery(product.queries.list({ source: 'latest', perPage: 8 }));
return (
<div className="s-products-list-wrapper s-products-list-vertical-cards">
{data?.items.map((item, index) => (
<ProductCard
key={item.id}
product={item}
index={index}
imagePriority={index < 2}
/>
))}
</div>
);
}
Example
import { useQuery } from '@tanstack/react-query';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
import { ProductCard } from '@salla.sa/twilight-theme-engine/components/product';
export function LatestProducts() {
const { data } = useQuery(product.queries.list({ source: 'latest', perPage: 8 }));
return (
<div className="s-products-list-wrapper s-products-list-vertical-cards">
{data?.items.map((item, index) => (
<ProductCard key={item.id} product={item} withShadow index={index} imagePriority={index < 2} />
))}
</div>
);
}
How it behaves
Not lazy:
ProductCardis a plainmemoexport and needs no<Suspense>. It readsuseMoney,useWishlist,useAsset,useNumber,useTranslationanduseTwilight, so it must render insideTwilightProvider.Price:
sale_pricewith theregular_pricestruck through whenis_on_sale, else "Starting from"starting_price, elseprice, all throughuseMoney().format. A donation product (donation.can_donate) shows no price.Badge, first match wins:
preorder.label,promotion_title, then withwithQuantity"Remained N" (quantity) or "Out of Stock" (is_out_of_stock).fullImageandminimalnever show a badge;specialadds a remaining-quantity pie whenquantityis set.The heart sits on the image for
vertical,minimalandspecial, and in the footer next to Add to cart forhorizontalandfullImage. The footer is gone withwithoutAddButton, and so is their heart.Add to cart is
SallaAddProductButton(the Salla web component) with the product id, status, type and pre-order flag: the SDK sends the request and shows its own toast.Image: srcset widths 150/300/450/600 with
sizesof(min-width: 768px) 25vw, 50vw, or(min-width: 1024px) 50vw, 100vwforfullImageandminimal. Passsizeswhen your grid has other columns. The fit class iss-product-card-image-<fit>, fromstore.settings.product.fit_type, elsecover.Links are plain
<a href={product.url}>. They navigate inside the app becauseTwilightProviderintercepts link clicks (client.interceptLinks, on by default).Every engine block that shows products (
ProductsSlider,FixedProducts,FeaturedProductsStyle2and3) and the product listing page render this component, so replacing it through the registry (product:card) changes them all.@salla.sa/twilight-theme-engine/productis the same module under a shorter name; the reference theme imports the card from there.
Gotchas
Replacing the card with
registry.override('product:card', MyCard)alone does nothing:ProductCarduses the key only whenregistry.getOriginal('product:card')is notnull, andoverriderecords an original only when the key already exists. Nothing in the engine registers it, so callregistry.register('product:card', ProductCard)first, thenoverride(src/components/product/ProductCard.tsx; packages/theme-tania/app/router.tsx does exactly this). See registry.Your replacement cannot render the engine
ProductCardinside itself: that call resolves the key again, finds your card, and recurses. To restyle rather than replace, wrap the engine card in your own component and use yours in your grids, as the reference theme does inapp/components/product/ProductCard.tsx.The registry lookup runs once per mounted card (
useMemowith no dependencies). Register at module scope inapp/router.tsx, before anything renders.A product with
quantity: 0in thespeciallayout prints a stray "0": the pie is guarded withisSpecial && product?.quantity && (…), and React renders the number0. A rating of0stars does the same in every layout, throughproduct.rating?.stars && (…).Salla's placeholder picture is cropped like a real one. The card means to show it with
contain, but it detects it withuseAsset().isPlaceholder(), which always returnsfalse(its placeholder URL is hard-coded tonull).
Related
A carousel block that loads products from a source you name (latest, offers, a category…) and draws a ProductCard for each.
registryThe shared name-to-component table the engine consults for a few swappable parts: the product card, the product gallery and home blocks.
useWishlistThe shopper's wishlist, shared by every component on the page: check a product, and add, remove or toggle it through the Salla SDK.
useMoneyFormats prices in the page language (with the riyal icon for SAR), and parses or validates amounts.