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.
Demo
Checked bags
Up to 23 kg each · 158 cm in all1 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.00
- Charles Babbage No checked bags
- Grace Hopper No checked bags
1 bag in allYou 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 fareExtra bags for each passenger: 1st 50,00 €
- Grace Hopper 1 bag · included
1 bag in all
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.qwemain.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>}Related
FareOptionsFare families side by side, Basic to Flex: the price a person and over the cheapest, what each includes, costs extra or leaves out, and the total for everyone.8 views · 11 downloadsFlightResultA flight in search results: departure and arrival times, the time in the air, stops and layovers, cabin and bags, and the fare with Select.9 views · 9 downloadsFlightSearchA flight search form: return or one way, from and to with a swap button and airport suggestions, dates, travellers and cabin, checked before it searches.8 views · 7 downloadsRoomPickerA hotel's rooms with their rates (non-refundable, free cancellation, breakfast) and a counter for each, rooms left, the stay's total, who it sleeps and Reserve.9 views · 9 downloadsSeatMapA plane's cabin to pick a seat for each passenger: aisles, window and aisle seats, taken seats, exit and extra legroom rows, a price per seat and the total.8 views · 7 downloadsCarRentalCardA hire car offer: class and model or similar, seats, bags, gearbox and air conditioning, mileage and fuel policy, supplier rating, pick-up and the price a day and in all.10 views · 11 downloads
Thread
Thread 0 posts
On BaggageSelector: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.