Day trip
Sintra and Cascais day trip with Pena Palace
Lasts 9 hours
- Free cancellation
- Hotel pickup
From was $85 $69 per person
A 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.
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/tour_card.qweimport ui/theme.qwe use theme_cssuse ui/tour_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>The code of the preview above.
view TourCardDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; align-items: start"> <TourCard id="tour-card-demo-sintra" title="Sintra and Cascais day trip with Pena Palace" category="Day trip" image="https://picsum.photos/seed/sintra/480/360" minutes={540} rating={4.8} reviews={3120} price={69.00m} old={85.00m} badge="Likely to sell out" badge_tone="warning" features={["Free cancellation", "Hotel pickup"]} languages={["English", "Spanish", "French"]} next="Tomorrow, 08:30" href="#sintra" save_href="#save-sintra"/> <TourCard id="tour-card-demo-food" title="Porto food and wine walk" category="Food & drink" image="https://picsum.photos/seed/porto-food/480/360" minutes={210} rating={4.95} reviews={1} price={58.50m} currency="EUR" locale="de-DE" features={["Skip the line"]} languages={["English"]} save_href="#saved" saved={true} href="#food"/> <TourCard id="tour-card-demo-boat" title="Private sunset sailing" duration="2 to 3 hours" price={240} per="per group of up to 6" from={false}/> </div>// TourCard: a tour or an activity to book: its photo with a badge ("Likely to sell// out") and a heart to save it, the kind of thing it is, its name, how long it takes,// the stars and reviews, what makes it easy (free cancellation, hotel pickup, skip the// line, languages spoken), when it next starts, and the price "from" per person.//// <TourCard title="Sintra and Cascais day trip" category="Day trip" image="/img/sintra.jpg" minutes={540}// rating={4.8} reviews={3120} price={69.00m} old={85.00m} badge="Likely to sell out"// features={["Free cancellation", "Hotel pickup"]} languages={["English", "Spanish"]} href="/tours/sintra"/>//// title: its name; href makes it a link (the whole card can be clicked). image,// image_alt: its photo (alt "" by default). category: a word over the name ("Walking// tour", "Day trip", "Food & drink"). minutes: how long it lasts (shown "3 hours",// "1.5 hours", "45 minutes"; from 24 hours on, in days), or duration: that as text ("2// to 3 hours"). rating: stars out of 5 (any fraction); reviews: how many. features:// short words, each with a tick ("Free cancellation", "Skip the line"). languages:// what the guide speaks. next: when it next starts ("Today, 14:00"). badge: a word on// the photo; badge_tone: "accent" (default), "warning" or "success". price: what it// costs (decimals best; numbers are read as decimals); old: before, struck through;// per: "per person" (default), "per group"; from: false drops "From". currency: ISO// code ("USD"); locale: how amounts are written ("en-US"). save_href: a heart on the// photo that links there (to save it to a list); saved: it is filled. id, class.import ui/theme.qwe use ui_clslet tour_card_css = css`.ui-tourcard { position: relative; display: flex; flex-direction: column; min-width: 0; max-width: 340px; 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-tourcard:hover { border-color: var(--ui-strong); box-shadow: var(--ui-shadow); }.ui-tourcard:has(.ui-tourcard-title a:focus-visible) { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-tourcard-media { position: relative; aspect-ratio: 4 / 3; overflow: hidden; background: var(--ui-subtle); }.ui-tourcard-media img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .25s; }.ui-tourcard:hover .ui-tourcard-media img { transform: scale(1.03); }.ui-tourcard-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: .02em; }.ui-tourcard-badge-warning { background: var(--ui-warning); color: var(--ui-surface); }.ui-tourcard-badge-success { background: var(--ui-success); color: var(--ui-surface); }.ui-tourcard-save { position: absolute; z-index: 1; top: 8px; right: 8px; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow); }.ui-tourcard-save svg { width: 18px; height: 18px; }.ui-tourcard-save:hover { color: var(--ui-danger); }.ui-tourcard-save:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-tourcard-saved { color: var(--ui-danger); }.ui-tourcard-saved svg { fill: currentColor; }.ui-tourcard-body { display: flex; flex: 1; flex-direction: column; gap: 7px; padding: 12px 14px 14px; }.ui-tourcard-cat { color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-tourcard .ui-tourcard-title { margin: 0; font-size: 16px; font-weight: 650; line-height: 1.3; }.ui-tourcard-title a { color: inherit; text-decoration: none; }.ui-tourcard-title a::after { content: ""; position: absolute; inset: 0; }.ui-tourcard-title a:hover { color: var(--ui-accent); }.ui-tourcard-title a:focus-visible { outline: none; }.ui-tourcard-facts { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; color: var(--ui-muted); font-size: 13px; }.ui-tourcard-facts > span { display: inline-flex; align-items: center; gap: 4px; }.ui-tourcard-facts svg { width: 15px; height: 15px; }.ui-tourcard-rating b { color: var(--ui-fg); font-weight: 650; }.ui-tourcard-rating svg { color: var(--ui-warning); }.ui-tourcard-feats { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; color: var(--ui-success); font-size: 13px; }.ui-tourcard-feats li::before { content: "✓ "; font-weight: 700; }.ui-tourcard-langs, .ui-tourcard-next { color: var(--ui-muted); font-size: 12px; }.ui-tourcard-next b { color: var(--ui-fg); font-weight: 600; }.ui-tourcard-price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; margin-top: auto; padding-top: 8px; font-variant-numeric: tabular-nums; }.ui-tourcard-from { color: var(--ui-muted); font-size: 12px; }.ui-tourcard-amount { font-size: 19px; font-weight: 700; letter-spacing: -.01em; }.ui-tourcard-old { color: var(--ui-danger); font-size: 13px; text-decoration: line-through; }.ui-tourcard-per { color: var(--ui-muted); font-size: 12px; }@media (prefers-reduced-motion: reduce) { .ui-tourcard, .ui-tourcard-media img { transition: none; } .ui-tourcard:hover .ui-tourcard-media img { transform: none; } }`let tour_card_icons = { clock: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>, 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>, heart: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 20s-7.5-4.6-9.2-9.3C1.6 7.3 3.9 4 7.3 4c2 0 3.6 1.1 4.7 2.7C13.1 5.1 14.7 4 16.7 4c3.4 0 5.7 3.3 4.5 6.7C19.5 15.4 12 20 12 20z"/></svg>,}// How long, in words: "45 minutes", "1 hour", "1.5 hours", "2 days".fn tour_card_length(m: int) -> str { if m < 60 { return `${m} minutes` } if m >= 1440 { let d = round(m / 144) / 10 return d == 1 ? "1 day" : `${d} days` } let h = round(m / 6) / 10 h == 1 ? "1 hour" : `${h} hours`}// An amount in its currency; whole: without the cents of a whole amount ("$69").fn tour_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 TourCard(title: str, price, image: str? = null, image_alt: str = "", href: str? = null, category: str? = null, minutes: int? = null, duration: str? = null, rating = null, reviews: int? = null, features: list = [], languages: list = [], next: str? = null, badge: str? = null, badge_tone: str = "accent", old = null, per: str? = "per person", from: bool = true, currency: str = "USD", locale: str = "en-US", save_href: str? = null, saved: bool = false, id: str? = null, class: str? = null) { style global tour_card_css let cost = dec(price) let before = old != null && dec(old) > cost ? dec(old) : null let length = duration ?? (minutes != null ? tour_card_length(minutes) : null) let stars = rating != null ? round(float(rating), 1) : null <article class={ui_cls(["ui-tourcard", class])} id={id}> {(image != null || badge != null || save_href != null) && <div.ui-tourcard-media> {image != null && <img src={image} alt={image_alt} loading="lazy"/>} {badge != null && <span class={ui_cls(["ui-tourcard-badge", badge_tone != "accent" && `ui-tourcard-badge-${badge_tone}`])}>{badge}</span>} {save_href != null && <a class={ui_cls(["ui-tourcard-save", saved && "ui-tourcard-saved"])} href={save_href} aria-label={saved ? `Saved: ${title}` : `Save ${title}`}>{tour_card_icons.heart}</a>} </div>} <div.ui-tourcard-body> {category != null && <span.ui-tourcard-cat>{category}</span>} <h3.ui-tourcard-title>{href != null ? <a href={href}>{title}</a> : title}</h3> {(length != null || stars != null) && <div.ui-tourcard-facts> {length != null && <span.ui-tourcard-length>{tour_card_icons.clock}<span.ui-sr>Lasts </span>{length}</span>} {length != null && stars != null && " "} {stars != null && <span.ui-tourcard-rating> {tour_card_icons.star} <span.ui-sr>Rated </span><b>{format_number(stars, locale, 1)}</b><span.ui-sr> out of 5</span> {reviews != null && [" ", <span>({format_number(reviews, locale, 0)}<span.ui-sr>{reviews == 1 ? " review" : " reviews"}</span>)</span>]} </span>} </div>} {len(features) > 0 && <ul.ui-tourcard-feats>{for f in features => <li>{f}</li>}</ul>} {len(languages) > 0 && <span.ui-tourcard-langs>{len(languages) == 1 ? "Language" : "Languages"}: {join(languages, ", ")}</span>} {next != null && <span.ui-tourcard-next>Next: <b>{next}</b></span>} <div.ui-tourcard-price> {from && <span.ui-tourcard-from>From</span>} {before != null && [" ", <s.ui-tourcard-old><span.ui-sr>was </span>{tour_card_money(before, currency, locale, true)}</s>]} {" "}<span.ui-tourcard-amount data-amount={str(cost)}>{tour_card_money(cost, currency, locale, true)}</span> {per != null && [" ", <span.ui-tourcard-per>{per}</span>]} </div> </div> </article>}On TourCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.