Travel

BaggageSelector

Checked bags for each passenger with counters, what the first, second and third bag cost, the cabin allowance, the total and the saving against the airport.

#travel#flights#forms#commerce#interactive

Demo

Preview

Checked bags

Up to 23 kg each · 158 cm in all

1 cabin bag up to 8 kg and 1 personal item, included

Bags for each passenger: 1st $35.00, 2nd $45.00, 3rd $70.00

  • Ada Lovelace 1 bag · $35.001
  • Charles Babbage No checked bags0
  • Grace Hopper No checked bags0
1 bag in all$35.00You save $35.00 on paying at the airport

Add a bag: on_change gets the counts and the total.

Extra luggage

Up to 32 kg each · 1 included in the fare

Extra bags for each passenger: 1st 50,00 €

  • Grace Hopper 1 bag · included1
1 bag in all0,00 €

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/baggage_selector.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/baggage_selector.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/baggage_selector.qwe
component BaggageSelectorDemoLive() {    state note = "Add a bag: on_change gets the counts and the total."    <div.demo-stack>        <BaggageSelector id="baggage-selector-demo" name="bags" passengers={["Ada Lovelace", "Charles Babbage", "Grace Hopper"]}                         value={[1, 0, 0]} prices={[35.00m, 45.00m, 70.00m]} weight="23 kg" size="158 cm in all"                         cabin="1 cabin bag up to 8 kg and 1 personal item" airport_price={70.00m}                         on_change={(counts, total) => { note = `on_change: ${join(counts, ", ")}; ${total} in all.` }}/>        <p.demo-note id="baggage-selector-demo-note">{note}</p>    </div>}view BaggageSelectorDemo() =    <div class="demo-row" style="align-items: start">        <BaggageSelectorDemoLive/>        <BaggageSelector id="baggage-selector-demo-eur" name="rail_bags" label="Extra luggage" passengers={["Grace Hopper"]} included={1}                         prices={[50]} weight="32 kg" currency="EUR" locale="de-DE"/>    </div>

Source

ui/baggage_selector.qwe
// BaggageSelector: checked bags for each passenger of a booking, with a counter for// each (− and +), what that passenger's bags cost (the first, second and third each// priced as airlines do, after those the fare includes), a button to give everyone the// first passenger's bags, the cabin allowance that comes anyway, and the total, with// what booking now saves against paying at the airport. A plain <form> gets each// passenger's count as `name`_0, `name`_1, ...////   <BaggageSelector passengers={["Ada Lovelace", "Charles Babbage"]} prices={[35.00m, 45.00m, 70.00m]} weight="23 kg"//                    cabin="1 cabin bag up to 8 kg and 1 personal item" airport_price={70.00m}/>//   <BaggageSelector passengers={["Grace Hopper"]} included={1} prices={[50]} currency="EUR" locale="de-DE"//                    on_change={(counts, total) => { bags = counts; extra = total }}/>//// passengers: their names, in order (["Passenger 1"]). value: the bags each starts// with (a list of counts; the bags included by default). prices: what each paid bag// costs, the first, the second, ... (decimals best; numbers are read as decimals);// their number is how many may be bought. included: bags the fare includes, free (0).// weight: what one bag may weigh ("23 kg"); size: its size ("158 cm in all"). cabin:// what goes in the cabin anyway (a line with a tick; null for none). airport_price:// what a bag costs at the airport, to say how much is saved. currency: ISO code// ("USD"); locale: how amounts are written ("en-US"). name: the fields' names// ("bags"). label: the heading ("Checked bags"). on_change(counts, total): inside one// of your components, each passenger's count and the total, a decimal, after each// change. id, class.import ui/theme.qwe use ui_clslet baggage_selector_css = css`.ui-bagsel { display: grid; gap: 14px; min-width: 0; max-width: 560px; padding: 18px 20px; 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); font-variant-numeric: tabular-nums; }.ui-bagsel-head { display: flex; align-items: center; gap: 12px; }.ui-bagsel-icon { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--ui-radius-sm); background: var(--ui-accent-soft);  color: var(--ui-accent); }.ui-bagsel-icon svg { width: 22px; height: 22px; }.ui-bagsel .ui-bagsel-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-bagsel-rule { display: block; color: var(--ui-muted); font-size: 13px; }.ui-bagsel-cabin { margin: 0; padding: 8px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-success-soft); font-size: 13px; }.ui-bagsel-cabin::before { content: "✓ "; color: var(--ui-success); font-weight: 700; }.ui-bagsel-prices { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-bagsel-rows { display: grid; margin: 0; padding: 0; list-style: none; }.ui-bagsel-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 16px; padding: 12px 0; border-top: 1px solid var(--ui-border); }.ui-bagsel-row:last-child { border-bottom: 1px solid var(--ui-border); }.ui-bagsel-who { display: grid; min-width: 0; }.ui-bagsel-who b { overflow: hidden; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }.ui-bagsel-cost { color: var(--ui-muted); font-size: 13px; }.ui-bagsel-count { display: flex; align-items: center; gap: 10px; }.ui-bagsel-count output { min-width: 22px; font-size: 16px; font-weight: 650; text-align: center; }.ui-bagsel-step { display: grid; place-items: center; width: 34px; height: 34px; padding: 0; border: 1px solid var(--ui-border); border-radius: 50%;  background: var(--ui-surface); color: var(--ui-fg); font: 18px/1 var(--ui-font); cursor: pointer; transition: border-color .15s, background .15s; }.ui-bagsel-step:hover:not(:disabled) { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-bagsel-step:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bagsel-step:disabled { opacity: .35; cursor: not-allowed; }.ui-bagsel-same { justify-self: start; padding: 0; border: 0; background: none; color: var(--ui-accent); font: 500 13px var(--ui-font); cursor: pointer; }.ui-bagsel-same:hover { text-decoration: underline; }.ui-bagsel-same:focus-visible { outline: none; border-radius: 2px; box-shadow: var(--ui-focus); }.ui-bagsel-foot { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; gap: 2px 12px; }.ui-bagsel-foot output { font-size: 20px; font-weight: 700; }.ui-bagsel-save { grid-column: 1 / -1; color: var(--ui-success); font-size: 13px; font-weight: 600; }@media (prefers-reduced-motion: reduce) { .ui-bagsel-step { transition: none; } }`let baggage_selector_bag = <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="6" width="14" height="14" rx="2"/><path d="M9 6V3h6v3M9 10v6M15 10v6M8 20v1.5M16 20v1.5"/></svg>// An amount in its currency ("$35.00", "50,00 €").fn baggage_selector_money(x: dec, currency: str, locale: str) -> str {    let m = money.of(x, currency, "half_up")    type_of(m) == "error" ? `${format_number(round(x, 2, "half_up"), locale, 2)} ${currency}` : money.format(m, locale)}// What n bags cost one passenger: the included ones free, then each paid one at its price.fn baggage_selector_cost(n: int, included: int, prices: list) -> dec {    let paid = n > included ? n - included : 0    sum(take(prices, paid) | map((p) => dec(p))) + 0m}component BaggageSelector(prices: list, passengers: list = ["Passenger 1"], value: list? = null, included: int = 0, weight: str? = "23 kg",                          size: str? = null, cabin: str? = null, airport_price = null, currency: str = "USD", locale: str = "en-US",                          name: str = "bags", label: str = "Checked bags", on_change = null, id: str? = null, class: str? = null) {    style global baggage_selector_css    let most = included + len(prices)    state counts = passengers | enumerate | map((p) => {        let v = value != null ? value[p[0]] ?? included : included        v < 0 ? 0 : (v > most ? most : v)    })    let base = `baggage-selector-${name}`    let costs = counts | map((n) => baggage_selector_cost(n, included, prices))    let total = sum(costs) + 0m    let paid = sum(counts | map((n) => n > included ? n - included : 0))    let bags = sum(counts)    let saved = airport_price != null ? dec(airport_price) * paid - total : null    let money_of = (x: dec) => baggage_selector_money(x, currency, locale)    let change = (next: list) => {        if next != counts {            counts = next            on_change && on_change(next, sum(next | map((n) => baggage_selector_cost(n, included, prices))) + 0m)        }    }    let step = (i: int, by: int) => {        let n = counts[i] + by        if n >= 0 && n <= most { change(set(counts, i, n)) }    }    let rule = join([weight != null ? `Up to ${weight} each` : null, size, included > 0 ? `${included} included in the fare` : null]        | filter((x) => x != null), " · ")    let price_text = prices | enumerate | map((p) => `${["1st", "2nd", "3rd", "4th", "5th"][p[0]] ?? `${p[0] + 1}th`} ${money_of(dec(p[1]))}`) | join(", ")    <section class={ui_cls(["ui-bagsel", class])} id={id} aria-labelledby={`${base}-title`}>        <div.ui-bagsel-head>            <span.ui-bagsel-icon>{baggage_selector_bag}</span>            <div>                <h3.ui-bagsel-title id={`${base}-title`}>{label}</h3>                {rule != "" && <span.ui-bagsel-rule>{rule}</span>}            </div>        </div>        {cabin != null && <p.ui-bagsel-cabin>{cabin}, included</p>}        {len(prices) > 0 && <p.ui-bagsel-prices>{included > 0 ? "Extra bags" : "Bags"} for each passenger: {price_text}</p>}        <ul.ui-bagsel-rows>            {for p in enumerate(passengers) => {                let i = p[0]                let who = p[1]                let n = counts[i]                let cost = costs[i]                <li.ui-bagsel-row key={i} data-passenger={i}>                    <span.ui-bagsel-who>                        <b id={`${base}-who-${i}`}>{who}</b>                        {" "}<span.ui-bagsel-cost>{n == 0 ? "No checked bags" : `${n == 1 ? "1 bag" : `${n} bags`} · ${cost == 0 ? "included" : money_of(cost)}`}</span>                    </span>                    <span.ui-bagsel-count role="group" aria-labelledby={`${base}-who-${i}`}>                        <button.ui-bagsel-step type="button" aria-label={`One bag fewer for ${who}`} disabled={n <= 0} @click={() => step(i, -1)}>−</button>                        <output aria-live="polite" aria-label={`${n} checked ${n == 1 ? "bag" : "bags"}`}>{n}</output>                        <button.ui-bagsel-step type="button" aria-label={`One bag more for ${who}`} disabled={n >= most} @click={() => step(i, 1)}>+</button>                    </span>                    <input type="hidden" name={`${name}_${i}`} value={str(n)}/>                </li>            }}        </ul>        {len(passengers) > 1 && <button.ui-bagsel-same type="button" disabled={all(counts, (n) => n == counts[0])}                                        @click={() => { change(counts | map((n) => counts[0])) }}>            Give everyone {counts[0] == 1 ? "1 bag" : `${counts[0]} bags`}, as {passengers[0]}        </button>}        <div.ui-bagsel-foot>            <span id={`${base}-total-label`}>{bags == 1 ? "1 bag" : `${bags} bags`} in all</span>            <output aria-labelledby={`${base}-total-label`} aria-live="polite" data-amount={str(total)}>{money_of(total)}</output>            {saved != null && saved > 0 && <span.ui-bagsel-save>You save {money_of(saved)} on paying at the airport</span>}        </div>    </section>}
Thread
98Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.