Product
Describes a product as the products API returns it: name, prices, images, stock flags, category, brand and tags.
import { Product, ProductType, ProductStatus, ProductImage, ProductRating, ProductCategory, ProductBrand, ProductTag } from '@salla.sa/twilight-theme-engine/types';In plain words
A type describes the shape of data: which fields an object has, and what kind of value each one holds. Product is the shape of every product the engine hands you, in a product grid, on the product page or in a home block.
You import it with import type. Your editor then autocompletes product.name and flags a typo such as product.nmae before the code runs. The type is removed when the theme is built, so it never checks the real data: the Salla API can send a value the type does not expect, and the live demo shows where it does.
A product comes in two sizes. A list card (from a product list, a home block or search) has the basics and one image. The details of one product (what the product page loads) add images, options and skus.
Signature
interface Product {
id: number; // a string on list cards: see Gotchas
name: string;
description: string; // HTML
url: string;
type: ProductType;
status: ProductStatus;
promotion_title?: string; subtitle?: string;
sku?: string | null; mpn?: string | null; gtin?: string | null;
weight?: string | null; calories?: number | null;
// Prices
price: number | string; // what the shopper pays now
regular_price: number; // the price before a sale
sale_price: number;
starting_price?: number | null;
base_currency_price: { currency: string; amount: number } | number;
currency: string;
discount_percentage?: string; discount_ends?: string;
// Stock
quantity?: number | null; sold_quantity?: number; max_quantity: number;
// Related data
image: ProductImage; images?: ProductImage[];
rating?: ProductRating; category?: ProductCategory; brand?: ProductBrand; tags?: ProductTag[];
options?: ProductOption[]; skus?: ProductSku[];
notify_availability?: NotifyAvailability; donation?: ProductDonation;
// Flags
is_available: boolean; is_out_of_stock: boolean; is_on_sale: boolean;
is_hidden_quantity: boolean; is_taxable: boolean; is_require_shipping: boolean;
has_options?: boolean; has_read_more: boolean; has_size_guide: boolean;
can_add_note: boolean; can_upload_file: boolean; giftable?: boolean; is_in_wishlist?: boolean;
// …and more flags, add_to_cart_label?, notes?, digital_files_settings?, preorder?
}
type ProductType =
| 'product' | 'service' | 'group_products' | 'codes' | 'digital'
| 'food' | 'donating' | 'booking' | 'financial_support';
type ProductStatus = 'sale' | 'out' | 'out-and-notify' | 'hidden';
interface ProductImage {
id?: number; url?: string; alt?: string; type?: 'image' | 'video';
video_url?: string; three_d_image_url?: string; main?: boolean; sort?: number;
}
interface ProductRating { count: number; stars: number }
interface ProductCategory {
id?: number; name: string; url: string; icon?: string;
image?: string | null; sub_categories?: Category[];
}
interface ProductBrand { id: number; name?: string; description?: string; url?: string; logo?: string }
interface ProductTag { id?: number; name: string; url: string }Try it live
Product declares. ✗ marks a value the type does not allow.Try this: turn on Only mismatches: id changes kind between the two columns, and a list card has no is_hidden_quantity.Loading the latest products…
import type { Product } from '@salla.sa/twilight-theme-engine/types';
/** Lists send ids as strings and details as numbers: compare them as strings. */
export function isSameProduct(a: Pick<Product, 'id'>, b: Pick<Product, 'id'>) {
return String(a.id) === String(b.id);
}
/** `sale_price` is null, 0 or a copy of `price` unless the product is on sale. */
export function wasPrice(product: Product): number | null {
return product.is_on_sale ? product.regular_price : null;
}
Example
import type { Product } from '@salla.sa/twilight-theme-engine/types';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
type PriceTagProps = {
// Pick keeps only the fields this component reads.
product: Pick<Product, 'price' | 'regular_price' | 'is_on_sale'>;
};
export function PriceTag({ product }: PriceTagProps) {
const { format } = useMoney();
return (
<p className="price-tag">
<strong>{format(product.price)}</strong>
{product.is_on_sale && <s>{format(product.regular_price)}</s>}
</p>
);
}
How it behaves
A type only:
import type { Product }is erased from the built theme. The same type is used byProductCard,ProductDetails,AddToCartForm,useProduct, the product context, andproduct.listandproduct.findin@salla.sa/twilight-theme-engine/api/product.List cards and details share this one type, which is why
images,options,skusand many flags are optional. On the demo store a list card has noimages,options,skus,base_currency_price,has_read_moreorcan_show_remained_quantity.Prices on the demo store:
priceis the amount to pay now,regular_pricethe price before a sale, andstarting_price, when set, equals the cheapest variant. The engineProductCardshowssale_priceonly whenis_on_saleistrue.base_currency_pricearrives as an object{ currency, amount }in details on the demo store. The engine reads it withtypeof product.base_currency_price === "object"(AddToCartForm,ProductDetails).quantitywas-1in details andnullon list cards for every demo store product checked. Stock per variant isskus[].stock_quantity; see ProductOption, ProductSku and friends.
Gotchas
idis typednumber, but list cards send it as a string ("1303461379") and details as a number (demo store), soa.id === b.idisfalsefor the same product. CompareString(a.id) === String(b.id), passString(id)toproduct.queries.detail, andNumber(id)touseWishlist().has.List cards spell the flag
is_hidded_quantity; only details sendis_hidden_quantity(demo store). On a cardproduct.is_hidden_quantityisundefined, so!product.is_hidden_quantityshows a stock count the merchant hid. Decide it from the details, or read both spellings.sale_priceis typednumber, but when the product is not on sale it isnull,0or a copy ofprice(demo store lists and details).product.sale_price ?? product.pricecan give0. Checkis_on_salefirst;pricealready holds the amount to pay.max_quantityis typednumberbut list cards sendnull, andbrand.idis typednumberbut details send a path such as"test-link/brand-1321220630"(demo store). Treat both as data to pass along, never as numbers to calculate with.Many optional fields arrive as
nullrather than missing:rating,quantity,category.urlon a card, andimage.urlfor a product without a photo (demo store).product.rating !== undefinedistruefornull; test with!= null, or use?..images[].typecan be"3d-image"(demo store details), which the"image" | "video"union does not list, so aswitchover those two cases skips it.docs/07-data-types.md shows
imagesandoptionsas required,is_giftable, anddiscount_ends?: Date. The code has optionalimagesandoptions,giftable, anddiscount_ends?: string:product.is_giftabledoes not compile.
Related
Describes a product's options and their choices, the SKU variants those choices select, back-in-stock alerts and donation progress.
product.listLists products from any source (latest, offers, a category, a brand, a search) with page size, sort, filters and cursor pagination.
product.findFetches one product with everything a product page needs: images, options, SKUs, brand, tags, rating and bundle contents.
ProductCardThe product tile of every grid: image, badge, name, price, rating, wishlist heart and a real Add to cart button, in five layouts.
useProductKeeps a live copy of a product that follows price and stock changes from Salla option pickers, and can reload its details.