Summary#
Mounted as it is, the listing is a storefront's front page:
[Route("/")] [AllowAnonymous]
component Front() { render { ShopSearchPage(); } }Each product is a card: its cover photograph when it has one, its name, its price (and the price before a sale, struck through), and — once five or fewer are left — how many: "3 left", "Only one left", "Sold out".
Signature#
ShopSearchPage(string productPrefix = "/p/", bool linkById = false, bool onlySale = false, int showStockUnder = 5)
ShopSearchPage(…) { slot card { c => … } } // c is a ShopCard
public class ShopCard {
Product Product; string Name; string Href;
string Photo; string PhotoAlt; // "" when the product has no photograph
string Price; string WasPrice; string PriceNote; // money as the shop writes it; WasPrice "" unless on sale
int? Left; bool SoldOut; string Stock; // Stock: "Sold out", "Only one left", "3 left" or ""
}
int? LeftOf(Variant[] variants); // null when it never runs out
string StockNote(Variant[] variants, int showUnder = 5);Description#
What a card says#
The photograph is the product's cover — its first ProductImage — read aloud by the words staff gave it. The price is
today's, in the shop's currency and its way of showing VAT. How much is left is the product's stock over all its
variants; a product that never runs out (a gift card, a file) says nothing. showStockUnder is the count at which a
card starts saying how many are left — 0 says only "Sold out".
A card of your own#
Fill the card slot and the listing draws your card for each product instead of its own. The search, the size filter,
the sorting and the grid stay the kit's, so nothing has to be rebuilt to change how a product looks:
ShopSearchPage(showStockUnder: 3) {
slot card { c =>
Link(href: c.Href, label: c.Name) {
Stack(gap: 2) {
if (c.Photo != "") { Image(src: c.Photo, alt: c.PhotoAlt, aspectRatio: "1 / 1", objectFit: ObjectFit.Cover); }
Text(c.Name, fontWeight: FontWeight.Bold);
Text(c.WasPrice == "" ? c.Price : c.Price + " — was " + c.WasPrice);
if (c.Stock != "") { Badge(c.Stock, tone: c.SoldOut ? Tone.Default : Tone.Warning); }
}
}
}
}c.Product is the product itself, for a field of your own subtype. The template is yours: it sees the page around it
as any slot template does.
The sale shelf#
ShopSearchPage(onlySale: true) lists only what is marked down, and says so when nothing is.
Examples#
A pottery's front page with its own card#
app Pottery {
use Osysharp.Ui;
use Osysharp.Workflow;
use Osysharp.Storage;
use Osysharp.Shop@1 { egress "api.resend.com"; secret "ResendApiKey"; }
use Osysharp.Identity@0;
use Osysharp.UserAccounts@1;
use Osysharp.Payments@0;
use Osysharp.Shipping@0;
model "model/**/*.osy";
}
using Osysharp.Ui;
using Osysharp.Shop;
[Route("/")] [AllowAnonymous]
component Front() {
render {
ShopSearchPage(showStockUnder: 3) {
slot card { c =>
Link(href: c.Href, label: c.Name) {
Stack(gap: 2, p: 3, bg: Colors.Surface, rounded: Radius.Md) {
if (c.Photo != "") { Image(src: c.Photo, alt: c.PhotoAlt, aspectRatio: "1 / 1", objectFit: ObjectFit.Cover); }
Text(c.Name, fontWeight: FontWeight.Bold);
Text(c.WasPrice == "" ? c.Price : c.Price + " — was " + c.WasPrice);
if (c.Stock != "") { Text(c.Stock, fontSize: FontSize.Label, color: Colors.TextMuted); }
}
}
}
}
}
}See also#
- Slot(item) — let the caller decide what each row looks like — how a component hands each row to its caller's template
- Building a look — from an empty kit to a design a store can wear — a whole storefront look on the Shop kit