Travel

FlightResult

A flight in search results: departure and arrival times, the time in the air, stops and layovers, cabin and bags, and the fare with Select.

#travel#flights#card#commerce#no-js

Demo

Preview
Best

TAP Air Portugal TP 1351 · TP 207: departs 07:45 from London, arrives 13:10 in New York, 12h 25m, 1 stop in Lisbon, $412.00 per person.

  • Economy · Classic
  • Cabin bag
  • 1 checked bag
was $468.00$412.00per personSelect
Cheapest

Quillo Air QA 88: departs 22:30 from Porto, arrives 02:05 the next day in Frankfurt, 2h 35m, nonstop, 89,99 € per person.

  • Economy · Light
  • No cabin bag
  • No checked bag
89,99 €per person3 seats left at this price

Nordic Wings: departs 06:10 from Copenhagen, arrives 07:55 the next day in Singapore, 18h 45m, 2 stops in Helsinki and Dubai, $1,240.00 2 travellers.

$1,240.002 travellers

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

Example

The code of the preview above.

demos/flight_result.qwe
view FlightResultDemo() =    <div.demo-stack>        <FlightResult id="flight-result-demo-stop" airline="TAP Air Portugal" flight="TP 1351 · TP 207" depart={{ code: "LHR", time: "07:45", city: "London" }}                      arrive={{ code: "JFK", time: "13:10", city: "New York" }} minutes={745}                      stops={[{ code: "LIS", city: "Lisbon", minutes: 110 }]} price={412.00m} old={468.00m} cabin="Economy" fare="Classic"                      bags={{ cabin: true, checked: 1 }} tag="Best" href="#book-tp1351"/>        <FlightResult id="flight-result-demo-night" airline="Quillo Air" flight="QA 88" depart={{ code: "OPO", time: "22:30", city: "Porto" }}                      arrive={{ code: "FRA", time: "02:05", city: "Frankfurt", days: 1 }} minutes={155} price={89.99m} currency="EUR" locale="de-DE"                      cabin="Economy" fare="Light" bags={{ cabin: false, checked: 0 }} tag="Cheapest" tag_tone="success" seats_left={3} action="#select"/>        <FlightResult id="flight-result-demo-two" airline="Nordic Wings" depart={{ code: "CPH", time: "06:10", city: "Copenhagen" }}                      arrive={{ code: "SIN", time: "07:55", city: "Singapore", days: 1 }} minutes={1125} price={1240} per="2 travellers"                      stops={[{ code: "HEL", city: "Helsinki", minutes: 85 }, { code: "DXB", city: "Dubai" }]}/>    </div>

Source

ui/flight_result.qwe
// FlightResult: one flight in a list of search results: the airline, the times of// departure and arrival large with the airports under them ("+1" when it lands on a// later day), the time in the air and the stops between (with where and how long), the// cabin and what bags come with the fare, and the price with a Select button. Screen// readers hear the whole flight as one sentence before the parts.////   <FlightResult airline="TAP Air Portugal" flight="TP 1351" depart={{ code: "LHR", time: "07:45", city: "London" }}//                 arrive={{ code: "JFK", time: "13:10", city: "New York", days: 0 }} minutes={745}//                 stops={[{ code: "LIS", city: "Lisbon", minutes: 110 }]} price={412.00m} href="/book/tp1351"/>//   <FlightResult airline="Quillo Air" depart={{ code: "OPO", time: "22:30" }} arrive={{ code: "FRA", time: "02:05", days: 1 }}//                 minutes={155} price={89.99m} currency="EUR" locale="de-DE" tag="Cheapest" seats_left={3} action="/select"/>//// airline: who flies it; flight: the flight number(s) ("TP 1351 · TP 207"); logo: the// airline's logo (a square picture; its initials stand in without one); operated_by:// "Operated by Portugália". depart, arrive: { code, time, city?, date? } (time as it// should read, "07:45"); arrive.days: how many days after departure it lands (1 shows// "+1"). minutes: the whole journey's length. stops: a list of { code, city?, minutes?// } for each stop on the way, minutes being the wait there ([] is nonstop). price:// what it costs (decimals best; numbers are read as decimals); old: the price before,// struck through. per: under the price ("per person"; null for none). currency: ISO// code ("USD"). locale: how amounts are written ("en-US"). cabin: "Economy",// "Business", ... fare: the fare's name ("Light"). bags: { cabin: true/false, checked:// 0, 1, 2 } (left out says nothing of bags). tag: a word at the top ("Cheapest",// "Fastest", "Best"); tag_tone: "accent" (default), "success" or "warning".// seats_left: "3 seats left at this price" when given (and under 10). href: Select is// a link there; action: else a form posting `flight` (the flight number) there;// neither: no button. select_label ("Select"). id, class.import ui/theme.qwe use ui_clslet flight_result_css = css`.ui-flres { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; min-width: 0; 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; transition: border-color .15s, box-shadow .15s; }.ui-flres:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }.ui-flres-tagged { border-color: var(--ui-flres-tag, var(--ui-accent)); }.ui-flres-main { display: grid; gap: 12px; min-width: 0; padding: 16px 18px; }.ui-flres-tag { position: absolute; top: -10px; left: 16px; height: 20px; padding: 0 9px; border-radius: 999px; background: var(--ui-flres-tag, var(--ui-accent));  color: var(--ui-accent-fg); font: 600 11px/20px var(--ui-font); letter-spacing: .03em; text-transform: uppercase; }.ui-flres-tag-success { --ui-flres-tag: var(--ui-success); }.ui-flres-tag-warning { --ui-flres-tag: var(--ui-warning); }.ui-flres-tag-success .ui-flres-tag, .ui-flres-tag-warning .ui-flres-tag { color: var(--ui-surface); }.ui-flres-airline { display: flex; align-items: center; gap: 10px; min-width: 0; color: var(--ui-muted); font-size: 13px; }.ui-flres-logo { flex: none; display: grid; place-items: center; width: 28px; height: 28px; overflow: hidden; border-radius: 6px; background: var(--ui-accent-soft);  color: var(--ui-accent); font-size: 11px; font-weight: 700; letter-spacing: .02em; }.ui-flres-logo img { width: 100%; height: 100%; object-fit: contain; }.ui-flres-airline b { color: var(--ui-fg); font-weight: 600; }.ui-flres-route { display: grid; grid-template-columns: auto minmax(90px, 1fr) auto; align-items: center; gap: 14px; }.ui-flres-end { display: grid; gap: 1px; }.ui-flres-end:last-child { text-align: right; }.ui-flres-time { font-size: 22px; font-weight: 700; line-height: 1.1; letter-spacing: -.01em; white-space: nowrap; }.ui-flres-days { margin-left: 2px; vertical-align: super; color: var(--ui-danger); font-size: 11px; font-weight: 700; }.ui-flres-code { color: var(--ui-muted); font-size: 13px; font-weight: 600; letter-spacing: .04em; }.ui-flres-path { display: grid; gap: 4px; justify-items: center; min-width: 0; color: var(--ui-muted); font-size: 12px; text-align: center; }.ui-flres-line { position: relative; display: flex; align-items: center; justify-content: space-evenly; width: 100%; height: 10px; }.ui-flres-line::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; border-top: 1.5px solid var(--ui-strong); }.ui-flres-line::after { content: ""; position: absolute; right: -2px; top: 50%; width: 6px; height: 6px; margin-top: -3px; border: solid var(--ui-strong);  border-width: 1.5px 1.5px 0 0; transform: rotate(45deg); }.ui-flres-stop { position: relative; width: 9px; height: 9px; border: 2px solid var(--ui-warning); border-radius: 50%; background: var(--ui-surface); }.ui-flres-stops { font-weight: 600; color: var(--ui-warning); }.ui-flres-nonstop .ui-flres-stops { color: var(--ui-success); }.ui-flres-via { overflow: hidden; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }.ui-flres-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-flres-meta li { display: inline-flex; align-items: center; gap: 5px; }.ui-flres-meta svg { width: 15px; height: 15px; }.ui-flres-meta .ui-flres-no { color: var(--ui-muted); opacity: .7; text-decoration: line-through; }.ui-flres-side { display: grid; align-content: center; justify-items: end; gap: 4px; min-width: 150px; padding: 16px 18px; border-left: 1px dashed var(--ui-border);  text-align: right; }.ui-flres-price { font-size: 22px; font-weight: 700; line-height: 1.1; letter-spacing: -.01em; white-space: nowrap; }.ui-flres-old { color: var(--ui-muted); font-size: 13px; text-decoration: line-through; }.ui-flres-per { color: var(--ui-muted); font-size: 12px; }.ui-flres-left { color: var(--ui-danger); font-size: 12px; font-weight: 600; }.ui-flres-form { margin: 4px 0 0; }.ui-flres-btn { display: inline-flex; align-items: center; justify-content: center; height: 36px; margin-top: 4px; padding: 0 18px; border: 0;  border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font: 600 14px var(--ui-font); text-decoration: none;  cursor: pointer; }.ui-flres-form .ui-flres-btn { margin-top: 0; }.ui-flres-btn:hover { filter: brightness(1.08); }.ui-flres-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (max-width: 560px) {  .ui-flres { grid-template-columns: 1fr; }  .ui-flres-side { grid-template-columns: 1fr auto; align-items: center; justify-items: start; border-left: 0; border-top: 1px dashed var(--ui-border); text-align: left; }  .ui-flres-side .ui-flres-btn, .ui-flres-side .ui-flres-form { grid-column: 2; grid-row: 1 / span 4; justify-self: end; }}@media (prefers-reduced-motion: reduce) { .ui-flres { transition: none; } }`// Minutes as "7h 25m", "45m", "3h".fn flight_result_duration(m: int) -> str {    let h = floor(m / 60)    let r = m % 60    h == 0 ? `${r}m` : (r == 0 ? `${h}h` : `${h}h ${r}m`)}// An amount in its currency ("$412.00", "89,99 €"); the code after it for one money does not know.fn flight_result_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)}// "TA" for "TAP Air Portugal": the first letters of the first two words.fn flight_result_initials(name: str) -> str =    upper(join(split(trim(name), " ") | filter((w) => w != "") | take(2) | map((w) => slice(w, 0, 1)), ""))let flight_result_icons = {    cabin: <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="6" y="7" width="12" height="13" rx="2"/><path d="M9 7V4h6v3M9 20v1M15 20v1"/></svg>,    checked: <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="15" rx="2"/><path d="M9 6V3h6v3M9 10v7M15 10v7"/></svg>,    seat: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 4v9a2 2 0 0 0 2 2h7l2 5M7 13h9"/></svg>,}view FlightResult(airline: str, depart: map, arrive: map, minutes: int, price, stops: list = [], flight: str? = null, logo: str? = null,                  operated_by: str? = null, old = null, per: str? = "per person", currency: str = "USD", locale: str = "en-US",                  cabin: str? = null, fare: str? = null, bags: map? = null, tag: str? = null, tag_tone: str = "accent",                  seats_left: int? = null, href: str? = null, action: str? = null, select_label: str = "Select",                  id: str? = null, class: str? = null) {    style global flight_result_css    let n = len(stops)    let days = arrive.days ?? 0    let stop_text = n == 0 ? "Nonstop" : (n == 1 ? "1 stop" : `${n} stops`)    let via = stops | map((s) => s.minutes != null ? `${s.code} ${flight_result_duration(s.minutes)}` : s.code) | join(", ")    let said_stops = n == 0 ? "nonstop" : `${stop_text} in ${stops | map((s) => s.city ?? s.code) | join(" and ")}`    let later = days == 1 ? " the next day" : (days > 1 ? ` ${days} days later` : (days < 0 ? " the day before" : ""))    let cost = flight_result_money(dec(price), currency, locale)    let sentence = `${airline}${flight != null ? ` ${flight}` : ""}: departs ${depart.time} from ${depart.city ?? depart.code}, arrives ${arrive.time}${later} in ${arrive.city ?? arrive.code}, ${flight_result_duration(minutes)}, ${said_stops}, ${cost}${per != null ? ` ${per}` : ""}.`    let checked = bags != null ? bags.checked ?? 0 : 0    <article class={ui_cls(["ui-flres", n == 0 && "ui-flres-nonstop", tag != null && "ui-flres-tagged", tag != null && tag_tone != "accent" && `ui-flres-tag-${tag_tone}`, class])}             id={id} aria-label={`${airline}, ${depart.code} to ${arrive.code}, ${depart.time}`}>        {tag != null && <span.ui-flres-tag>{tag}</span>}        <div.ui-flres-main>            <p.ui-sr>{sentence}</p>            <div.ui-flres-airline aria-hidden="true">                <span.ui-flres-logo>{logo != null ? <img src={logo} alt=""/> : flight_result_initials(airline)}</span>                <span><b>{airline}</b>{flight != null && ` · ${flight}`}{operated_by != null && <span> · {operated_by}</span>}</span>            </div>            <div.ui-flres-route aria-hidden="true">                <div.ui-flres-end>                    <span.ui-flres-time>{depart.time}</span>                    <span.ui-flres-code title={depart.city}>{depart.code}</span>                </div>                <div.ui-flres-path>                    <span.ui-flres-duration>{flight_result_duration(minutes)}</span>                    <span.ui-flres-line>{for s in stops => <span.ui-flres-stop title={s.city ?? s.code}></span>}</span>                    <span><span.ui-flres-stops>{stop_text}</span>{n > 0 && <span.ui-flres-via> · {via}</span>}</span>                </div>                <div.ui-flres-end>                    <span.ui-flres-time>{arrive.time}{days != 0 && <sup.ui-flres-days>{days > 0 ? `+${days}` : str(days)}</sup>}</span>                    <span.ui-flres-code title={arrive.city}>{arrive.code}</span>                </div>            </div>            {(cabin != null || fare != null || bags != null) && <ul.ui-flres-meta>                {(cabin != null || fare != null) && <li>{flight_result_icons.seat}{join([cabin, fare] | filter((x) => x != null), " · ")}</li>}                {bags != null && <li class={bags.cabin == false ? "ui-flres-no" : null}>{flight_result_icons.cabin}{bags.cabin == false ? "No cabin bag" : "Cabin bag"}</li>}                {bags != null && <li class={checked == 0 ? "ui-flres-no" : null}>{flight_result_icons.checked}{checked == 0 ? "No checked bag" : (checked == 1 ? "1 checked bag" : `${checked} checked bags`)}</li>}            </ul>}        </div>        <div.ui-flres-side>            {old != null && dec(old) > dec(price) && <span.ui-flres-old><span.ui-sr>was </span>{flight_result_money(dec(old), currency, locale)}</span>}            <span.ui-flres-price data-amount={str(dec(price))}>{cost}</span>            {per != null && <span.ui-flres-per>{per}</span>}            {seats_left != null && seats_left > 0 && seats_left < 10 && <span.ui-flres-left>{seats_left == 1 ? "1 seat left" : `${seats_left} seats left`} at this price</span>}            {href != null                ? <a.ui-flres-btn href={href} aria-label={`${select_label}: ${airline} ${depart.time} to ${arrive.code}, ${cost}`}>{select_label}</a>                : (action != null                    ? <form.ui-flres-form method="post" action={action}>                          <input type="hidden" name="flight" value={flight ?? `${depart.code}-${arrive.code}-${depart.time}`}/>                          <button.ui-flres-btn type="submit" aria-label={`${select_label}: ${airline} ${depart.time} to ${arrive.code}, ${cost}`}>{select_label}</button>                      </form>                    : null)}        </div>    </article>}
Thread
99Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.