Travel

HotelCard

A hotel in search results: photo, stars, the review score badge, location, amenities, cancellation terms and the price a night with the stay's total.

#travel#hotels#card#commerce#no-js

Demo

Preview
Deal
Hotel
Alfama, Lisbon · 0.8 km from the centre
  • Free WiFi
  • Breakfast included
  • Air conditioning
  • Rooftop terrace
Free cancellation until 3 NovNo prepayment needed
Only 2 rooms left at this price
Excellent 1,284 reviews8.7
was $152$129 / night4 nights: $557.28 with taxes
Apartment

Riverside flat

Porto, Ribeira
  • Kitchen
  • Washing machine
Exceptional 1 review9,6
95 € / night3 nights: 285,00 €Includes taxes and fees

Harbour Inn

Dover
Fair6.4
$74.50 / night+ $9.20 taxes and fees

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

Example

The code of the preview above.

demos/hotel_card.qwe
view HotelCardDemo() =    <div.demo-stack>        <HotelCard id="hotel-card-demo-alfama" name="Casa do Alfama" kind="Hotel" stars={4} score={8.7} reviews={1284} location="Alfama, Lisbon"                   distance="0.8 km from the centre" image="https://picsum.photos/seed/alfama/480/360" badge="Deal" price={129.00m} old={152.00m}                   nights={4} taxes={41.28m} amenities={["Free WiFi", "Breakfast included", "Air conditioning", "Rooftop terrace"]}                   cancellation="Free cancellation until 3 Nov" prepay="No prepayment needed" rooms_left={2} href="#casa-do-alfama"/>        <div class="demo-row" style="align-items: start">            <HotelCard id="hotel-card-demo-flat" layout="column" name="Riverside flat" kind="Apartment" score={9.6} reviews={1}                       location="Porto, Ribeira" image="https://picsum.photos/seed/ribeira/480/300" price={95} currency="EUR" locale="de-DE"                       nights={3} price_note="Includes taxes and fees" amenities={["Kitchen", "Washing machine"]}/>            <HotelCard id="hotel-card-demo-plain" layout="column" name="Harbour Inn" stars={2} score={6.4} score_label="Fair" price={74.50m}                       location="Dover" price_note="+ $9.20 taxes and fees" rooms_left={8}/>        </div>    </div>

Source

ui/hotel_card.qwe
// HotelCard: a place to stay in a list of results: its photo, name, star rating and// where it is, the guests' review score as a badge with its word ("8.7 Excellent") and// how many reviews, what it has (free WiFi, breakfast, a pool), the cancellation// terms, how few rooms are left, and the price a night with the stay's total.////   <HotelCard name="Casa do Alfama" stars={4} score={8.7} reviews={1284} location="Alfama, Lisbon"//              distance="0.8 km from the centre" image="/img/alfama.jpg" price={129.00m} nights={4}//              amenities={["Free WiFi", "Breakfast", "Air conditioning"]} cancellation="Free cancellation until 3 Nov"//              href="/hotels/casa-do-alfama"/>//// name: the hotel; href makes it a link (the whole card can be clicked). image,// image_alt: its photo (alt is "" by default: the name says what it is; without a// photo the card is its text alone). kind: a word over the name ("Hotel", "Apartment",// "Guest house"). stars: its class, 1 to 5 (null for none). score: the guests' rating// out of 10; reviews: how many; score_label: the word beside it (worked out from the// score: Exceptional, Excellent, Very good, Good, Pleasant, Review score). location:// where; distance: how far from what matters. amenities: a few words each.// cancellation: the terms, in green ("Free cancellation until 3 Nov"); prepay: a line// under it ("No prepayment needed"). rooms_left: "Only 2 rooms left" under 5. badge: a// word on the photo ("Deal", "Popular"). price: a night, decimals best (numbers are// read as decimals); old: a night before, struck through. nights: shows the total for// the stay (with taxes when given); taxes: what is added to the total; price_note:// under the price ("Includes taxes and fees"). currency: ISO code ("USD"); locale: how// amounts are written ("en-US"). layout: "row" (default: the photo beside the rest;// stacked on a narrow screen) or "column". action_label: the button's text ("See// availability"; shown only with href). id, class.import ui/theme.qwe use ui_clslet hotel_card_css = css`.ui-hotelcard { position: relative; display: grid; grid-template-columns: minmax(160px, 240px) minmax(0, 1fr); min-width: 0; overflow: hidden;  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);  transition: border-color .15s, box-shadow .15s; }.ui-hotelcard:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }.ui-hotelcard:has(.ui-hotelcard-name a:focus-visible) { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-hotelcard-column { display: flex; flex-direction: column; width: 100%; max-width: 340px; }.ui-hotelcard-bare { grid-template-columns: 1fr; }.ui-hotelcard-media { position: relative; min-height: 180px; background: var(--ui-subtle); }.ui-hotelcard-column .ui-hotelcard-media { aspect-ratio: 16 / 10; min-height: 0; }.ui-hotelcard-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }.ui-hotelcard-badge { position: absolute; top: 10px; left: 10px; height: 22px; padding: 0 9px; border-radius: 999px; background: var(--ui-accent);  color: var(--ui-accent-fg); font: 600 11px/22px var(--ui-font); letter-spacing: .03em; text-transform: uppercase; }.ui-hotelcard-body { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px 16px; padding: 14px 16px; }.ui-hotelcard-column .ui-hotelcard-body { flex: 1; grid-template-columns: 1fr; grid-template-rows: 1fr auto; }.ui-hotelcard-info { display: grid; gap: 6px; align-content: start; min-width: 0; }.ui-hotelcard-kind { color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-hotelcard-titlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }.ui-hotelcard .ui-hotelcard-name { margin: 0; font-size: 17px; font-weight: 650; line-height: 1.25; }.ui-hotelcard-name a { color: inherit; text-decoration: none; }.ui-hotelcard-name a::after { content: ""; position: absolute; inset: 0; }.ui-hotelcard-name a:hover { color: var(--ui-accent); }.ui-hotelcard-name a:focus-visible { outline: none; }.ui-hotelcard-stars { display: inline-flex; gap: 1px; color: var(--ui-warning); }.ui-hotelcard-stars svg { width: 13px; height: 13px; }.ui-hotelcard-where { color: var(--ui-muted); font-size: 13px; }.ui-hotelcard-amen { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }.ui-hotelcard-amen li { padding: 2px 8px; border: 1px solid var(--ui-border); border-radius: 999px; color: var(--ui-muted); font-size: 12px; }.ui-hotelcard-terms { display: grid; gap: 1px; font-size: 13px; }.ui-hotelcard-cancel { color: var(--ui-success); font-weight: 600; }.ui-hotelcard-cancel::before { content: "✓ "; }.ui-hotelcard-prepay { color: var(--ui-muted); }.ui-hotelcard-left { color: var(--ui-danger); font-size: 13px; font-weight: 600; }.ui-hotelcard-side { display: grid; align-content: space-between; justify-items: end; gap: 12px; text-align: right; }.ui-hotelcard-column .ui-hotelcard-side { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; text-align: left; }.ui-hotelcard-column .ui-hotelcard-price { justify-items: end; text-align: right; }.ui-hotelcard-review { display: flex; align-items: center; gap: 8px; }.ui-hotelcard-words { display: grid; font-size: 12px; color: var(--ui-muted); text-align: right; }.ui-hotelcard-words b { color: var(--ui-fg); font-size: 14px; font-weight: 650; }.ui-hotelcard-column .ui-hotelcard-review { flex-direction: row-reverse; }.ui-hotelcard-column .ui-hotelcard-words { text-align: left; }.ui-hotelcard-score { display: grid; place-items: center; min-width: 36px; height: 34px; padding: 0 6px; border-radius: 8px 8px 8px 2px; background: var(--ui-accent);  color: var(--ui-accent-fg); font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }.ui-hotelcard-price { display: grid; gap: 1px; justify-items: end; font-variant-numeric: tabular-nums; }.ui-hotelcard-old { color: var(--ui-danger); font-size: 13px; text-decoration: line-through; }.ui-hotelcard-night { font-size: 22px; font-weight: 700; line-height: 1.15; letter-spacing: -.01em; white-space: nowrap; }.ui-hotelcard-night small { color: var(--ui-muted); font-size: 12px; font-weight: 500; letter-spacing: 0; }.ui-hotelcard-total, .ui-hotelcard-note { color: var(--ui-muted); font-size: 12px; }.ui-hotelcard-btn { position: relative; z-index: 1; display: inline-flex; align-items: center; height: 34px; margin-top: 6px; padding: 0 14px;  border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font-size: 13px; font-weight: 600; text-decoration: none; }.ui-hotelcard-btn:hover { filter: brightness(1.08); }.ui-hotelcard-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (max-width: 620px) {  .ui-hotelcard { grid-template-columns: 1fr; }  .ui-hotelcard-media { aspect-ratio: 16 / 9; min-height: 0; }  .ui-hotelcard-body { grid-template-columns: 1fr; }  .ui-hotelcard-side { justify-items: start; text-align: left; }  .ui-hotelcard-price { justify-items: start; }}@media (prefers-reduced-motion: reduce) { .ui-hotelcard { transition: none; } }`let hotel_card_star = <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg>// The word for a review score out of 10.fn hotel_card_word(score: num) -> str {    if score >= 9.5 { return "Exceptional" }    if score >= 8.5 { return "Excellent" }    if score >= 8 { return "Very good" }    if score >= 7 { return "Good" }    if score >= 6 { return "Pleasant" }    "Review score"}// An amount in its currency; whole: without the cents of a whole amount ("$129").fn hotel_card_money(x: dec, currency: str, locale: str, whole: bool = false) -> str {    let m = money.of(x, currency, "half_up")    if type_of(m) == "error" { return `${format_number(round(x, 2, "half_up"), locale, 2)} ${currency}` }    let text = money.format(m, locale)    let units = money.minor_units(currency)    !whole || units == 0 || x != floor(x) ? text : re.replace(text, `[.,]0{${units}}(\\D*)$`, "$1")}view HotelCard(name: str, price, image: str? = null, image_alt: str = "", href: str? = null, kind: str? = null, stars: int? = null,               score = null, reviews: int? = null, score_label: str? = null, location: str? = null, distance: str? = null,               amenities: list = [], cancellation: str? = null, prepay: str? = null, rooms_left: int? = null, badge: str? = null,               old = null, nights: int? = null, taxes = null, price_note: str? = null, currency: str = "USD", locale: str = "en-US",               layout: str = "row", action_label: str = "See availability", id: str? = null, class: str? = null) {    style global hotel_card_css    let night = dec(price)    let before = old != null && dec(old) > night ? dec(old) : null    let total = nights != null ? night * nights + (taxes != null ? dec(taxes) : 0m) : null    let rated = score != null ? round(float(score), 1) : null    let shown = rated != null ? format_number(rated, locale, 1) : null    <article class={ui_cls(["ui-hotelcard", layout == "column" && "ui-hotelcard-column", image == null && "ui-hotelcard-bare", class])} id={id}>        {image != null && <div.ui-hotelcard-media>            <img src={image} alt={image_alt} loading="lazy"/>            {badge != null && <span.ui-hotelcard-badge>{badge}</span>}        </div>}        <div.ui-hotelcard-body>            <div.ui-hotelcard-info>                {kind != null && <span.ui-hotelcard-kind>{kind}</span>}                <div.ui-hotelcard-titlerow>                    <h3.ui-hotelcard-name>{href != null ? <a href={href}>{name}</a> : name}</h3>                    {stars != null && stars > 0 && <span.ui-hotelcard-stars role="img" aria-label={`${stars}-star ${lower(kind ?? "hotel")}`}>                        {for i in range(stars) => hotel_card_star}                    </span>}                </div>                {(location != null || distance != null) && <div.ui-hotelcard-where>{join([location, distance] | filter((x) => x != null), " · ")}</div>}                {len(amenities) > 0 && <ul.ui-hotelcard-amen aria-label="Amenities">{for a in amenities => <li>{a}</li>}</ul>}                {(cancellation != null || prepay != null) && <div.ui-hotelcard-terms>                    {cancellation != null && <span.ui-hotelcard-cancel>{cancellation}</span>}                    {prepay != null && <span.ui-hotelcard-prepay>{prepay}</span>}                </div>}                {rooms_left != null && rooms_left > 0 && rooms_left < 5 && <span.ui-hotelcard-left>{rooms_left == 1 ? "Only 1 room left" : `Only ${rooms_left} rooms left`} at this price</span>}            </div>            <div.ui-hotelcard-side>                {rated != null ? <div.ui-hotelcard-review>                    <span.ui-hotelcard-words>                        <b>{score_label ?? hotel_card_word(rated)}</b>                        {reviews != null && [" ", <span>{format_number(reviews, locale, 0)} {reviews == 1 ? "review" : "reviews"}</span>]}                    </span>                    <span.ui-hotelcard-score role="img" aria-label={`Scored ${shown} out of 10`}>{shown}</span>                </div> : <span></span>}                <div.ui-hotelcard-price>                    {before != null && <span.ui-hotelcard-old><span.ui-sr>was </span>{hotel_card_money(before, currency, locale, true)}</span>}                    <span.ui-hotelcard-night data-amount={str(night)}>{hotel_card_money(night, currency, locale, true)} <small>/ night</small></span>                    {total != null && <span.ui-hotelcard-total data-amount={str(total)}>                        {nights == 1 ? "1 night" : `${nights} nights`}: {hotel_card_money(total, currency, locale)}{taxes != null && " with taxes"}                    </span>}                    {price_note != null && <span.ui-hotelcard-note>{price_note}</span>}                    {href != null && <a.ui-hotelcard-btn href={href} tabindex="-1" aria-hidden="true">{action_label}</a>}                </div>            </div>        </div>    </article>}
Thread
910Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.