@doany-ai/catalog
v0.1.0
Published
What a business sells, on a Doany site: catalog hooks and default components (item cards, grid, variant picker, item detail)
Readme
@doany-ai/catalog
What a business sells, on its site: hooks that read the catalog through the
SDK client given to @doany-ai/app-core's <DoanyProvider>, and default
components a site can restyle through the theme, adjust through props, or
replace while keeping the hooks.
The site only reads the catalog. What is published, purchasable and at what price is the platform's to decide; the components show what the API answers. A visitor only ever sees published items; a site lists its own items and those every site of the business sells. Items are added and put on sale by the agent's business tools, never from the site's code.
Needs @doany-ai/app-core (DoanyProvider) and a client from
@doany-ai/sdk 0.3.0-alpha.0 or later. It ships no CSS: the site's Tailwind
build scans it (see @doany-ai/app-core).
import { CatalogGrid, CatalogItemDetail } from "@doany-ai/catalog";
import { BuyButton } from "@doany-ai/commerce";
<CatalogGrid
params={{ purchasable: true }}
hrefFor={(item) => `/services/${item.id}`}
renderLink={({ href, className, children }) => <Link to={href} className={className}>{children}</Link>}
/>
<CatalogItemDetail
itemId={id}
actions={(variant, item) => variant && item.purchasable && <BuyButton items={[{ catalog_item_variant_id: variant.id }]}>Order now</BuyButton>}
/>Records
CatalogItem:id,kind(service/physical/digital/plan/bundle),status(draft/active/paused/archived),name,description,category,image_url,service_mode(booking/order/inquiry, ornullwhen unclassified), optionalpayment_requirement(none/full_online/null),direct_purchase_enabled,purchasable(the API decides website eligibility),sort_order,variants(in display order). An item that was a product of a site built before the catalog also carrieslegacy_id(the id that site knew it by) andattributes(the fields the site kept on it beside its name and price, such asslugorcolor, as it kept them). Both are read only, and{}/nullon an item made in the catalog.CatalogVariant:id,name,price_amount(integer, smallest currency unit;null= no price,0= free),currency,duration_minutes,is_default(preselected),status,sku,sort_order.
Booking uses Calendar; Inquiry requires a confirmed quote. Their direct purchase is
fixed off. Order services have direct purchase fixed on, but charged website Orders
currently require full_online; none is available for merchant-entered orders.
Zero is free and null means no price. Follow the API's purchasable result.
Filter explicit Order services with params={{ service_mode: "order", purchasable: true }}
or useCatalogItems({ service_mode: "order" }). A service inquiry form itself creates
no order or appointment.
Components
Every component takes locale (money; default: the browser's), labels (a
part of CatalogLabels, see below) and className.
CatalogGrid
Cards in a grid, with loading, empty and error states and "Load more".
| Prop | Type | |
| --- | --- | --- |
| params | { purchasable?, category?, kind?, type?, service_mode?, q?, sort?, limit? } | which items. sort: sort_order (default), name, created_at, updated_at, - in front for descending. limit 1–200, default 50 |
| hrefFor | (item) => string | each card's link |
| renderLink | ({ href, className, children }) => ReactNode | how a link is rendered; a plain <a> by default. Pass the router's Link to navigate in the site |
| onSelect | (item) => void | makes each card a button instead |
| renderItem | (item) => ReactNode | replaces the default card |
| footer | ReactNode \| (item) => ReactNode | under each card's price |
| showDescription | boolean | |
| empty | ReactNode | shown when nothing is for sale. Default: the empty label |
CatalogItemCard
Image, name, category, price (From … when variants differ) and duration.
| Prop | Type | |
| --- | --- | --- |
| item | CatalogItem | required |
| href | string | makes the whole card a link |
| onSelect | (item) => void | makes the whole card a button |
| renderLink, footer, showDescription | | as CatalogGrid |
CatalogItemDetail
An item's page: image, name, description, the variant picker and the price of the chosen variant.
| Prop | Type | |
| --- | --- | --- |
| itemId | string | read by id… |
| item | CatalogItem | …or shown as given |
| actions | (variant \| null, item) => ReactNode | under the options: what to do with the chosen variant, e.g. a BuyButton |
| notFound | ReactNode | the item does not exist or is not published. Default: the notAvailable label |
VariantPicker
The priced active variants as a radio group; nothing for an item sold one way.
| Prop | Type | |
| --- | --- | --- |
| variants | CatalogVariant[] | required |
| value | string \| null | the chosen variant's id |
| onChange | (variantId) => void | required |
| includeUnpriced | boolean | also offer variants without a price (they cannot be ordered). Default false |
| label | string | for screen readers; wins over options |
Price
variant (one price) or item (one price, or From … when its variants
differ); fallback when there is no price (default: nothing).
Hooks
| Hook | Returns |
| --- | --- |
| useCatalogItems(params) | the site's items, a page at a time, sort_order first: items (all pages read so far), isLoading, isError, hasNextPage, fetchNextPage(). params as CatalogGrid |
| useCatalogItem(itemId) | a query; data is the item with its active variants |
| useVariantSelection(item) | { variant, select(variantId) }: starts on the item's default variant, follows a new item; an id that is not the item's falls back to the default |
Display helpers the components use: itemPriceLabel(item, locale?, words?),
variantPriceLabel(variant, locale?, words?) (text, or null when there is no
price), formatDuration(minutes) (1 h 30 min), pricedVariants(item),
defaultVariant(item). catalogKeys are the React Query keys, for
invalidating.
Labels
Every word the components show is in CatalogLabels. A component takes a part
of it and keeps the defaults for the rest, and passes it on to the ones inside
it (a grid to its cards, an item page to its picker). defaultCatalogLabels
holds the defaults; catalogLabels(partial) merges.
| Key | Default |
| --- | --- |
| free | Free |
| fromPrice | (price) => "From " + price |
| duration | (minutes) => "45 min", "1 h 30 min" |
| options | Options (the picker, for screen readers) |
| empty | Nothing here yet. |
| loadError | The catalog could not be loaded. (unless set, the platform's own message is shown) |
| tryAgain | Try again |
| loadMore | Load more |
| loadingMore | Loading… |
| notAvailable | This item is not available. |
Customizing
Stop at the first level that does what is needed:
CSS: the theme variables on
:root(see@doany-ai/app-core).Props: the slots and props above, and
labelsfor every word:<CatalogGrid labels={{ loadMore: "Show more", free: "Complimentary", duration: (m) => `${m} minutes` }} />Hooks: build the list or the item page on
useCatalogItems,useCatalogItemanduseVariantSelection, withitemPriceLabel/variantPriceLabelfor the prices.
Never edit this package under node_modules or copy its source into the site.
Develop
npm run typecheck includes tests/sdk-contract.types.ts: the real
@doany-ai/sdk client (from ../doany-sdk) must satisfy the client type this
package calls, so a changed SDK contract fails here, not on a site.
