CouponInput
A promo code box with Apply: on_apply(code) says whether it took, and an applied code shows with Remove.
Demo
No code applied. Try SAVE10, FREESHIP or EXPIRED.
Welcome gift: 15% off your first order
Get it
It needs theme.qwe, as every component does; 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/coupon_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/coupon_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/coupon_input.qwe
// Codes the demo knows: SAVE10 and FREESHIP take, EXPIRED says so, anything else is unknown.fn coupon_input_demo_check(code: str) -> map = match code { "SAVE10" => { ok: true, message: "10% off applied" } "FREESHIP" => { ok: true, message: "Free shipping applied" } "EXPIRED" => { ok: false, message: "That code has expired" } _ => { ok: false, message: `"${code}" is not a code we know` }}component CouponInputDemoLive() { state code = null <div.demo-stack.demo-narrow> <CouponInput name="coupon-demo" on_apply={(c) => { let r = coupon_input_demo_check(c) if r.ok { code = c } r }} on_remove={() => { code = null }}/> <p.demo-note>{code == null ? "No code applied. Try SAVE10, FREESHIP or EXPIRED." : `Code ${code} is applied (on_apply).`}</p> </div>}view CouponInputDemo() = <div.demo-stack> <CouponInputDemoLive/> <div.demo-narrow> <CouponInput name="coupon-demo-applied" applied="WELCOME" message="Welcome gift: 15% off your first order" size="sm"/> </div> </div>Source
ui/coupon_input.qwe
// CouponInput: a box for a promo code with an Apply button, saying whether the// code took. The code is a real <input> named `name`, so a plain <form> sends// it; Enter applies it (and does not submit the form around it).//// <CouponInput on_apply={(code) => check_code(code)}/>// <CouponInput applied="SAVE10" message="10% off" on_remove={() => { code = null }}/>//// on_apply(code): inside one of your components, given the code typed (upper// case, trimmed). What it gives back is shown: { ok: true, message: "10% off// applied" } marks the code applied and says so in green; { ok: false,// message: "That code has expired" } says so in red and keeps the box for// another try; null says nothing. It may be a server fn, or call one: the button// waits while it runs. on_remove(): called when an applied code's Remove is// clicked. applied: a code applied to start with, with message under it.// name ("coupon"), label ("Promo code"), placeholder, button ("Apply"),// disabled, size ("sm" or "md") as for any field. id: the input's id.import ui/theme.qwe use ui_clslet coupon_input_css = css`.ui-coupon { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-coupon-label { font-size: 13px; font-weight: 500; }.ui-coupon-row { display: flex; gap: 8px; }.ui-coupon-box { flex: 1; display: flex; align-items: center; height: 38px; padding: 0 12px; gap: 8px; min-width: 0; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); transition: border-color .15s, box-shadow .15s; }.ui-coupon-box:hover { border-color: var(--ui-strong); }.ui-coupon-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-coupon-box input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent; color: var(--ui-fg); font: 14px var(--ui-mono); letter-spacing: .04em; text-transform: uppercase; }.ui-coupon-box input::placeholder { color: var(--ui-muted); opacity: 1; font-family: var(--ui-font); letter-spacing: 0; text-transform: none; }.ui-coupon-box input:read-only { color: var(--ui-success); font-weight: 600; }.ui-coupon-ok { flex: none; width: 16px; height: 16px; color: var(--ui-success); }.ui-coupon-btn { flex: none; height: 38px; padding: 0 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); color: var(--ui-fg); font: 500 14px/1 var(--ui-font); cursor: pointer; transition: background .15s, border-color .15s; }.ui-coupon-btn:hover:not(:disabled) { border-color: var(--ui-strong); background: var(--ui-surface); }.ui-coupon-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-coupon-btn:disabled { opacity: .55; cursor: not-allowed; }.ui-coupon-invalid .ui-coupon-box, .ui-coupon-invalid .ui-coupon-box:hover { border-color: var(--ui-danger); }.ui-coupon-applied .ui-coupon-box { border-color: var(--ui-success); background: var(--ui-success-soft); }.ui-coupon-msg { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-coupon-msg-ok { color: var(--ui-success); }.ui-coupon-msg-bad { color: var(--ui-danger); }.ui-coupon-sm .ui-coupon-box, .ui-coupon-sm .ui-coupon-btn { height: 30px; font-size: 13px; }.ui-coupon-sm .ui-coupon-box { padding: 0 10px; }.ui-coupon-sm .ui-coupon-btn { padding: 0 12px; }.ui-coupon-disabled { opacity: .55; }@media (prefers-reduced-motion: reduce) { .ui-coupon-box, .ui-coupon-btn { transition: none; } }`component CouponInput(name: str = "coupon", label: str? = "Promo code", placeholder: str? = "Enter a code", button: str = "Apply", applied: str? = null, message: str? = null, disabled: bool = false, size: str = "md", id: str? = null, on_apply = null, on_remove = null, class: str? = null) { style global coupon_input_css state code = applied ?? "" state done = applied state said = applied != null ? message : null state ok = applied != null state busy = false let iid = id ?? `coupon-input-${name}` let apply = () => { let c = upper(trim(code)) if c == "" || busy { return null } busy = true let r = on_apply != null ? on_apply(c) : null busy = false if r == null { said = null } else if r.ok == true { done = c code = c ok = true said = r.message } else { ok = false said = r.message } } let remove = () => { done = null code = "" said = null ok = false on_remove && on_remove() } <div class={ui_cls(["ui-coupon", done != null && "ui-coupon-applied", said != null && !ok && "ui-coupon-invalid", size == "sm" && "ui-coupon-sm", disabled && "ui-coupon-disabled", class])}> {label != null && <label.ui-coupon-label for={iid}>{label}</label>} <div.ui-coupon-row> <div.ui-coupon-box> <input type="text" id={iid} name={name} value={code} placeholder={done == null ? placeholder : null} readonly={done != null} disabled={disabled} autocomplete="off" autocapitalize="characters" spellcheck="false" aria-invalid={said != null && !ok ? "true" : null} aria-describedby={said != null ? `${iid}-msg` : null} @input={(e) => { code = e.value; if said != null && !ok { said = null } }} @keydown={(e) => { if e.key == "Enter" { e.preventDefault(); if done == null { apply() } } }}/> {done != null && <svg.ui-coupon-ok viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>} </div> {done != null ? <button.ui-coupon-btn type="button" disabled={disabled} aria-label={`Remove code ${done}`} @click={remove}>Remove</button> : <button.ui-coupon-btn type="button" disabled={disabled || busy || trim(code) == ""} aria-busy={busy ? "true" : null} @click={apply}>{busy ? "Checking…" : button}</button>} </div> <p class={ui_cls(["ui-coupon-msg", said != null && (ok ? "ui-coupon-msg-ok" : "ui-coupon-msg-bad")])} id={`${iid}-msg`} role={said != null && !ok ? "alert" : "status"}>{said ?? ""}</p> </div>}Related
AddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsCartItemOne 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 downloadsPaymentMethodsWays to pay as radio cards with their marks: card (with the brands taken), PayPal, Apple Pay, Google Pay, bank, cash.6 views · 6 downloadsShippingOptionsShipping methods as radio cards, each with its price and how long it takes; a plain form sends the choice.7 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloadsCurrencyInputAn amount with its currency's symbol, thousands grouped as it is typed and the decimals the currency has, sent as plain digits.5 views · 5 downloads
Thread
Thread 0 posts
On CouponInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.