Commerce

CouponInput

A promo code box with Apply: on_apply(code) says whether it took, and an applied code shows with Remove.

#commerce#checkout#forms#input#interactive

Demo

Preview

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.qwe
main.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>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.