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.
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.
Demo
Quillo Air QA 88: departs 22:30 from Porto, arrives 02:05 the next day in Frankfurt, 2h 35m, nonstop, 89,99 € per person.
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.qwemain.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>}Related
BaggageSelectorChecked 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.9 views · 8 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 downloadsFareOptionsFare 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 downloadsFlightStatusA flight's status today: on time, delayed, boarding, in the air or cancelled, with the new times beside the old, gate changes and the baggage belt.9 views · 12 downloadsHotelCardA hotel in search results: photo, stars, the review score badge, location, amenities, cancellation terms and the price a night with the stay's total.9 views · 10 downloadsTourCardA tour or activity: photo, badge and save heart, duration, stars and reviews, free cancellation and pickup, languages, the next start and the price from per person.8 views · 9 downloads
Thread
Thread 0 posts
On FlightResult: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.