Sale
ProductCard
A product in a shop's grid: picture, name, price with the discount, stars, a badge and Add to cart, as a plain form or a callback.
Demo
New
Insulated bottle, 750 ml
32,50 €
Rain jacket
Out of stock$149.00
Northwind
Wool beanie
Only a few left$24.00
Nothing added yet.
Get it
It needs theme.qwe, as every component does, and price.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/price.qwe$ curl -O https://quillolang.org/components/ui/product_card.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/product_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/product_card.qwe
component ProductCardDemoLive() { state added = 0 <div.demo-stack> <div style="max-width: 240px"> <ProductCard id="beanie" name="Wool beanie" brand="Northwind" price={24} image="https://picsum.photos/seed/beanie/400/300" rating={4} reviews={37} stock="low" on_add={() => { added += 1 }}/> </div> <p.demo-note>{added == 0 ? "Nothing added yet." : `Added ${added} ${added == 1 ? "time" : "times"} (on_add).`}</p> </div>}view ProductCardDemo() = <div.demo-stack> <div.demo-row style="align-items: stretch"> <div style="width: 240px"> <ProductCard id="runner" name="Trail runner" brand="Northwind" price={89} old={120} currency="USD" image="https://picsum.photos/seed/runner/400/300" rating={4.5} reviews={128} badge="Sale" badge_tone="danger" href="#product_card" action="/cart"/> </div> <div style="width: 240px"> <ProductCard id="bottle" name="Insulated bottle, 750 ml" price={32.5} currency="EUR" locale="de-DE" image="https://picsum.photos/seed/bottle/400/300" rating={3.8} reviews={9} badge="New" ratio="1"/> </div> <div style="width: 240px"> <ProductCard id="jacket" name="Rain jacket" price={149} image="https://picsum.photos/seed/jacket/400/300" stock="out" action="/cart"/> </div> </div> <ProductCardDemoLive/> </div>Source
ui/product_card.qwe
// ProductCard: a product in a shop's grid: its picture, name, price (with the// old one and the discount), rating, a badge and an Add to cart button. The// price is written as Price writes it (this file needs price.qwe).//// <ProductCard id="sku-1" name="Trail runner" price={89} old={120} image="/p/1.jpg"// rating={4.5} reviews={128} badge="New" href="/products/sku-1" action="/cart"/>// <ProductCard name="Wool beanie" price={24} image="/p/2.jpg" stock="low" on_add={() => { added += 1 }}/>//// id: the product's id. name: its name; href makes it a link. brand: a quieter// line above the name. image, alt: its picture (alt is the name when left out).// price, old, currency, locale, cents: as for Price. rating: stars out of 5, any// fraction; reviews: how many, shown "(128)". badge: a word on the picture// ("New", "Sale"); badge_tone: "accent" (default), "success", "warning",// "danger" or "neutral". stock: "in" (default), "low" ("Only a few left") or// "out" (the button is disabled and says "Sold out").// action: a URL the button posts to as a plain <form method="post"> with the// product's id as `product`; without one the button calls on_add(), inside one// of your components. add_label: the button's text ("Add to cart").// corner: HTML at the picture's top-right (a WishlistButton). ratio: the// picture's shape ("4/3"; "1", "3/4"). size: "sm" or "md" (default).import ui/price.qwe use Priceimport ui/theme.qwe use ui_clslet product_card_css = css`.ui-product { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.4 var(--ui-font); overflow: hidden; transition: border-color .15s, box-shadow .15s; }.ui-product:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }.ui-product-media { position: relative; display: block; aspect-ratio: var(--r, 4 / 3); background: var(--ui-subtle); overflow: hidden; }.ui-product-media img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .25s; }.ui-product:hover .ui-product-media img { transform: scale(1.03); }.ui-product-badge { position: absolute; top: 10px; left: 10px; z-index: 1; height: 22px; padding: 0 8px; border-radius: 999px; font: 600 11px/22px var(--ui-font); letter-spacing: .02em; text-transform: uppercase; color: var(--ui-accent-fg); background: var(--ui-accent); }.ui-product-badge-success { background: var(--ui-success); color: #fff; }.ui-product-badge-warning { background: var(--ui-warning); color: #fff; }.ui-product-badge-danger { background: var(--ui-danger); color: #fff; }.ui-product-badge-neutral { background: var(--ui-fg); color: var(--ui-bg); }.ui-product-corner { position: absolute; top: 8px; right: 8px; z-index: 1; }.ui-product-out .ui-product-media img { opacity: .55; filter: grayscale(.6); }.ui-product-body { display: grid; gap: 6px; padding: 12px 14px 14px; flex: 1; align-content: start; }.ui-product-brand { font-size: 12px; color: var(--ui-muted); text-transform: uppercase; letter-spacing: .04em; }.ui-product-name { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.3; }.ui-product-name a { color: inherit; text-decoration: none; }.ui-product-name a::after { content: ""; position: absolute; inset: 0; }.ui-product-name a:hover { color: var(--ui-accent); }.ui-product-name a:focus-visible { outline: none; }.ui-product:has(.ui-product-name a:focus-visible) { box-shadow: var(--ui-focus); border-color: var(--ui-accent); }.ui-product { position: relative; }.ui-product-rating { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ui-muted); }.ui-product-stars { display: inline-flex; gap: 1px; }.ui-product-star { position: relative; width: 14px; height: 14px; color: var(--ui-strong); }.ui-product-star svg { display: block; width: 14px; height: 14px; }.ui-product-star-fill { position: absolute; inset: 0 auto 0 0; overflow: hidden; color: var(--ui-warning); }.ui-product-stock { font-size: 12px; font-weight: 500; color: var(--ui-warning); }.ui-product-out .ui-product-stock { color: var(--ui-danger); }.ui-product-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; padding-top: 6px; }.ui-product-form { position: relative; z-index: 1; display: contents; }.ui-product-btn { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 12px; border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font: 500 13px/1 var(--ui-font); white-space: nowrap; cursor: pointer; transition: background .15s, box-shadow .15s; }.ui-product-btn svg { width: 16px; height: 16px; }.ui-product-btn:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-product-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-product-btn:disabled { background: var(--ui-subtle); color: var(--ui-muted); cursor: not-allowed; }.ui-product-sm .ui-product-body { padding: 10px 12px 12px; gap: 4px; }.ui-product-sm .ui-product-name { font-size: 14px; }.ui-product-sm .ui-product-btn { height: 30px; padding: 0 10px; font-size: 12px; }@media (prefers-reduced-motion: reduce) { .ui-product, .ui-product-media img, .ui-product-btn { transition: none; } .ui-product:hover .ui-product-media img { transform: none; } }`let product_card_star = <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg>// Five stars with `value` of them filled (any fraction), as a picture screen readers hear.fn product_card_stars(value: num, reviews: int? = null) = <span.ui-product-rating> <span.ui-product-stars role="img" aria-label={`${value} out of 5 stars${reviews != null ? `, ${reviews} reviews` : ""}`}> {for i in range(5) => { let part = value - i < 0 ? 0 : value - i > 1 ? 1 : value - i <span.ui-product-star>{product_card_star}<span.ui-product-star-fill style={`width: ${round(part * 100)}%`}>{product_card_star}</span></span> }} </span> {reviews != null && <span aria-hidden="true">({reviews})</span>} </span>let product_card_cart = <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M3 4h2l2.4 11.2a1 1 0 0 0 1 .8h8.9a1 1 0 0 0 1-.8L20 8H6.5M10 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm7 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>view ProductCard(name: str, price, old = null, id: str? = null, brand: str? = null, image: str? = null, alt: str? = null, href: str? = null, currency: str = "USD", locale: str? = null, cents: bool = true, rating: num? = null, reviews: int? = null, badge: str? = null, badge_tone: str = "accent", stock: str = "in", action: str? = null, add_label: str = "Add to cart", on_add = null, corner = null, ratio: str = "4/3", size: str = "md", class: str? = null) { style global product_card_css let out = stock == "out" let button = <button.ui-product-btn type={action != null ? "submit" : "button"} disabled={out} aria-label={out ? null : `${add_label}: ${name}`} @click={() => { if action == null && on_add != null { on_add() } }}> {!out && product_card_cart}{out ? "Sold out" : add_label} </button> <article class={ui_cls(["ui-product", out && "ui-product-out", size == "sm" && "ui-product-sm", class])} data-product={id}> <div.ui-product-media style={`--r: ${ratio}`}> {badge != null && <span class={ui_cls(["ui-product-badge", badge_tone != "accent" && `ui-product-badge-${badge_tone}`])}>{badge}</span>} {image != null && <img src={image} alt={alt ?? name} loading="lazy"/>} {corner != null && <span.ui-product-corner>{corner}</span>} </div> <div.ui-product-body> {brand != null && <span.ui-product-brand>{brand}</span>} <h3.ui-product-name>{href != null ? <a href={href}>{name}</a> : name}</h3> {rating != null && product_card_stars(rating, reviews)} {stock == "low" && <span.ui-product-stock>Only a few left</span>} {out && <span.ui-product-stock>Out of stock</span>} <div.ui-product-foot> <Price amount={price} old={old} currency={currency} locale={locale} cents={cents}/> {action != null ? <form.ui-product-form method="post" action={action}> {id != null && <input type="hidden" name="product" value={id}/>} {button} </form> : button} </div> </div> </article>}Related
CartItemOne line of a cart: picture, name and options, a quantity control, the line's total and Remove, as inputs a form sends.6 views · 6 downloadsCartSummarySubtotal, shipping, tax, a discount and the total added up, with a Checkout button as a link, a form or a callback.6 views · 7 downloadsPricingCardOne plan: name, price and period, what it includes, and a button; highlighted with a badge when it is the one to pick.7 views · 6 downloadsProductGridProducts as ProductCards in a grid that fits as many across as there is room for, from a list of records.6 views · 5 downloadsSubscriptionCardThe plan someone is on: price, status, renewal date, usage bars and the links to manage, change or cancel it.6 views · 6 downloadsAddressCardA saved address with a Default badge and Edit and Remove, as submit buttons in a plain form or callbacks.6 views · 7 downloads
Thread
Thread 0 posts
On ProductCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.