Commerce

OrderHistory

Past orders with number, date, status and total, each opening on <details> to show its items and links.

#commerce#list#record#status#no-js

Demo

Preview
Order #10482Shipped$113.00, 2 items
  • Trail runnerQty 1 · Size 42, Black$89.00
  • Wool beanieQty 1$24.00
Order #10477Delivered$97.50, 3 items
  • Insulated bottle, 750 mlQty 3$32.50
Order #10391Refunded$149.00, 1 item
  • Rain jacketQty 1$149.00

Refunded to your card on 9 Sep.

Order #10120Cancelled¥2,400, 1 item
  • Merino socks, 3 pairsQty 1¥2,400
No orders 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/order_history.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/order_history.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/order_history.qwe
let order_history_demo_orders = [    { id: "10482", date: "2026-10-20", status: "shipped", total: 113, href: "#order_history", tracking_href: "#order_tracker",      items: [{ name: "Trail runner", quantity: 1, price: 89, image: "https://picsum.photos/seed/runner/80/80", options: ["Size 42", "Black"] },              { name: "Wool beanie", quantity: 1, price: 24, image: "https://picsum.photos/seed/beanie/80/80" }] },    { id: "10477", date: "2026-10-12", status: "delivered", total: 97.5, href: "#order_history",      items: [{ name: "Insulated bottle, 750 ml", quantity: 3, price: 32.5, image: "https://picsum.photos/seed/bottle/80/80" }] },    { id: "10391", date: "2026-09-02", status: "refunded", total: 149, href: "#order_history", note: "Refunded to your card on 9 Sep.",      items: [{ name: "Rain jacket", quantity: 1, price: 149 }] },    { id: "10120", date: "2026-07-15", status: "cancelled", total: 2400, currency: "JPY",      items: [{ name: "Merino socks, 3 pairs", quantity: 1, price: 2400 }] },]view OrderHistoryDemo() =    <div.demo-stack>        <OrderHistory orders={order_history_demo_orders} open={0}/>        <OrderHistory orders={[]}/>    </div>

Source

ui/order_history.qwe
// OrderHistory: past orders, one line each with its number, date, status and// total, opening (on <details>, so with no script) to show what was in it and// the links to the order and its tracking. Prices are written as Price writes// them (this file needs price.qwe).////   <OrderHistory orders={[//       { id: "10482", date: "2026-10-20", status: "delivered", total: 113, href: "/orders/10482",//         items: [{ name: "Trail runner", quantity: 1, price: 89, image: "/p/1.jpg" }, { name: "Wool beanie", quantity: 1, price: 24 }] },//       { id: "10477", date: "2026-10-12", status: "shipped", total: 32.5, tracking_href: "https://...", items: [...] }]}/>//// orders: a list of { id, date, status, total, items, href?, tracking_href?,// currency?, note? }: date an ISO date (written for locale); status one of// "processing", "paid", "shipped", "delivered", "cancelled", "refunded" or// "returned" (status_text in the order says it otherwise); items a list of// { name, quantity?, price?, image?, options? }. The newest first is up to// you. open: the index of the order open to start with (none). currency,// locale, cents as for Price. empty: what to say with no orders ("No orders// yet"). label: what screen readers call the list ("Orders").import ui/price.qwe use price_textimport ui/theme.qwe use ui_clslet order_history_css = css`.ui-orders { display: grid; gap: 10px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-orders-item { border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); }.ui-orders-item > summary { display: grid; grid-template-columns: auto 1fr auto auto auto; gap: 12px 16px; align-items: center; padding: 12px 16px; cursor: pointer; list-style: none; border-radius: var(--ui-radius); }.ui-orders-item > summary::-webkit-details-marker { display: none; }.ui-orders-item > summary:hover { background: var(--ui-subtle); }.ui-orders-item > summary:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-orders-chev { width: 8px; height: 8px; border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(-45deg); transition: transform .2s; }.ui-orders-item[open] > summary .ui-orders-chev { transform: rotate(45deg); }.ui-orders-id { font-weight: 600; }.ui-orders-id span { font-weight: 400; color: var(--ui-muted); }.ui-orders-date { color: var(--ui-muted); white-space: nowrap; }.ui-orders-status { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 9px; border-radius: 999px; font: 500 12px/1 var(--ui-font); white-space: nowrap;  --t: var(--ui-muted); --ts: var(--ui-subtle); color: var(--t); background: var(--ts); }.ui-orders-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--t); }.ui-orders-processing, .ui-orders-paid { --t: var(--ui-warning); --ts: var(--ui-warning-soft); }.ui-orders-shipped { --t: var(--ui-info); --ts: var(--ui-info-soft); }.ui-orders-delivered { --t: var(--ui-success); --ts: var(--ui-success-soft); }.ui-orders-cancelled { --t: var(--ui-danger); --ts: var(--ui-danger-soft); }.ui-orders-total { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-orders-body { padding: 4px 16px 14px; border-top: 1px solid var(--ui-border); }.ui-orders-lines { margin: 0; padding: 0; list-style: none; }.ui-orders-line { display: grid; grid-template-columns: 40px 1fr auto; gap: 12px; align-items: center; padding: 8px 0; }.ui-orders-line + .ui-orders-line { border-top: 1px solid var(--ui-border); }.ui-orders-line img, .ui-orders-ph { display: block; width: 40px; height: 40px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); object-fit: cover; }.ui-orders-line-text { display: grid; gap: 1px; min-width: 0; }.ui-orders-line-meta { font-size: 12px; color: var(--ui-muted); }.ui-orders-line-price { font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-orders-note { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-orders-links { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 10px; font-size: 13px; }.ui-orders-links a { color: var(--ui-accent); font-weight: 500; text-decoration: none; }.ui-orders-links a:hover { text-decoration: underline; }.ui-orders-empty { padding: 32px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius); color: var(--ui-muted); text-align: center; }@media (max-width: 600px) { .ui-orders-item > summary { grid-template-columns: auto 1fr auto; } .ui-orders-date, .ui-orders-total { grid-column: 2 / -1; } }@media (prefers-reduced-motion: reduce) { .ui-orders-chev { transition: none; } }`let order_history_words = { processing: "Processing", paid: "Paid", shipped: "Shipped", delivered: "Delivered", cancelled: "Cancelled", refunded: "Refunded", returned: "Returned" }view OrderHistory(orders: list = [], open: int? = null, currency: str = "USD", locale: str? = null, cents: bool = true,                  empty: str = "No orders yet", label: str = "Orders", class: str? = null) {    style global order_history_css    len(orders) == 0        ? <div class={ui_cls(["ui-orders-empty", class])} role="status">{empty}</div>        : <div class={ui_cls(["ui-orders", class])} role="list" aria-label={label}>            {for [i, o] in enumerate(orders) => {                let cur = get(o, "currency") ?? currency                let items = get(o, "items") ?? []                let count = sum(items | map((it) => get(it, "quantity") ?? 1))                <details.ui-orders-item key={o.id} role="listitem" open={i == open}>                    <summary>                        <span.ui-orders-chev aria-hidden="true"></span>                        <span.ui-orders-id><span>Order </span>#{o.id}</span>                        <span.ui-orders-date>{date.valid(o.date) ? <time datetime={o.date}>{date.format(o.date, { locale: locale ?? "en-GB", style: "medium" })}</time> : o.date}</span>                        <span class={`ui-orders-status ui-orders-${o.status}`}>{get(o, "status_text") ?? get(order_history_words, o.status) ?? o.status}</span>                        <span.ui-orders-total>{price_text(o.total, cur, locale, cents)}<span.ui-sr>, {count} {count == 1 ? "item" : "items"}</span></span>                    </summary>                    <div.ui-orders-body>                        <ul.ui-orders-lines>                            {for [j, it] in enumerate(items) => <li.ui-orders-line key={j}>                                {get(it, "image") != null ? <img src={it.image} alt="" loading="lazy"/> : <span.ui-orders-ph aria-hidden="true"></span>}                                <span.ui-orders-line-text>                                    <span>{it.name}</span>                                    <span.ui-orders-line-meta>                                        {`Qty ${get(it, "quantity") ?? 1}`}{len(get(it, "options") ?? []) > 0 && ` · ${join(it.options, ", ")}`}                                    </span>                                </span>                                {get(it, "price") != null && <span.ui-orders-line-price>{price_text(it.price, cur, locale, cents)}</span>}                            </li>}                        </ul>                        {get(o, "note") != null && <p.ui-orders-note>{o.note}</p>}                        {(get(o, "href") != null || get(o, "tracking_href") != null) && <div.ui-orders-links>                            {get(o, "href") != null && <a href={o.href}>View order<span.ui-sr> #{o.id}</span></a>}                            {get(o, "tracking_href") != null && <a href={o.tracking_href}>Track package<span.ui-sr> of order #{o.id}</span></a>}                        </div>}                    </div>                </details>            }}        </div>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

On OrderHistory: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.