News

PaywallMeter

A metered paywall: a slim bar counting the free articles read, a warning on the last, then the gate with the offer, its price, benefits and sign-in.

#news#subscription#commerce#banner#no-js

Demo

Preview

You've read all 5 free articles this month

Subscribe for $1.00 a week to keep reading.

  • Unlimited articles on any device
  • The daily crossword and the archive since 1921
  • Cancel anytime
Subscribe

Already a subscriber? Sign in

Your free articles come back on Nov 1.

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

Example

The code of the preview above.

demos/paywall_meter.qwe
view PaywallMeterDemo() =    <div.demo-stack style="gap: 20px">        <PaywallMeter id="paywall-meter-demo-bar" used={2} limit={5} subscribe_href="#subscribe" signin_href="#login"/>        <PaywallMeter id="paywall-meter-demo-last" used={5} limit={5} subscribe_href="#subscribe"/>        <div style="padding-top: 70px">            <PaywallMeter id="paywall-meter-demo-gate" used={6} limit={5} price={1.00m} price_period="week"                          benefits={["Unlimited articles on any device", "The daily crossword and the archive since 1921", "Cancel anytime"]}                          resets="Your free articles come back on Nov 1." subscribe_href="#subscribe?offer=intro" signin_href="#login"/>        </div>    </div>

Source

ui/paywall_meter.qwe
// PaywallMeter: the metered paywall's two faces. While free articles are left, a slim// bar: a dot for each free article, those read filled, "You have read 2 of 5 free// articles this month", and Subscribe; on the last one it says so. Once they are used// up, the gate: what happened, the offer with its price, what a subscription brings,// Subscribe, and a way in for subscribers. A view: the server counts, the page shows.////   <PaywallMeter used={2} limit={5} subscribe_href="/subscribe" signin_href="/login"/>//   <PaywallMeter used={6} limit={5} price={1.00m} price_period="week" resets="Your free articles come back on Nov 1."//                 benefits={["Unlimited articles on any device", "The daily crossword", "Cancel anytime"]}//                 subscribe_href="/subscribe?offer=intro" signin_href="/login"/>//// used: the free articles read in the period, this one included; limit: how many are// free. At the limit this one is the last ("This is your last free article"); past it,// the gate shows instead. period: "this month" (default), "this week". subscribe_href: where Subscribe// goes; subscribe_label: its text ("Subscribe"). signin_href: the link for those who// already pay ("Sign in"). price: the offer's price, a decimal (12.50m), in currency// ("USD") and written for locale ("en-US"); price_period: "week", "month" ("month"),// so it reads "$1.00 a week". offer: the gate's line about the offer, instead of// "Subscribe for $1.00 a week."; benefits: a few lines of what a subscription brings,// ticked. resets: a line on when free articles come back. title: the gate's heading,// instead of "You've read all 5 free articles this month". id: the element's id.import ui/theme.qwe use ui_clslet paywall_meter_css = css`.ui-pwm { color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-pwm-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; max-width: 760px; padding: 10px 14px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow); }.ui-pwm-dots { display: inline-flex; gap: 5px; flex: none; }.ui-pwm-dot { width: 10px; height: 10px; border: 2px solid var(--ui-accent); border-radius: 50%; }.ui-pwm-dot-on { background: var(--ui-accent); }.ui-pwm-last .ui-pwm-dot { border-color: var(--ui-warning); }.ui-pwm-last .ui-pwm-dot-on { background: var(--ui-warning); }.ui-pwm-text { flex: 1 1 220px; margin: 0; }.ui-pwm-text b { font-weight: 700; }.ui-pwm-actions { display: flex; align-items: center; gap: 12px; flex: none; }.ui-pwm-sub { display: inline-flex; align-items: center; justify-content: center; height: 36px; padding: 0 16px; border-radius: 999px;  background: var(--ui-accent); color: var(--ui-accent-fg); font-weight: 650; text-decoration: none; white-space: nowrap; }.ui-pwm-sub:hover { filter: brightness(1.08); }.ui-pwm-in { color: var(--ui-fg); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--ui-strong); }.ui-pwm-in:hover { color: var(--ui-accent); }.ui-pwm a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-pwm-gate { position: relative; display: grid; justify-items: center; gap: 12px; max-width: 560px; padding: 28px 28px 24px; text-align: center;  border: 1px solid var(--ui-border); border-top: 4px solid var(--ui-accent); border-radius: var(--ui-radius); background: var(--ui-surface);  box-shadow: var(--ui-shadow-lg); }.ui-pwm-gate::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 90px; pointer-events: none;  background: linear-gradient(to bottom, transparent, var(--ui-bg)); margin-bottom: 4px; }.ui-pwm-gate .ui-pwm-dots { margin-bottom: 2px; }.ui-pwm .ui-pwm-title { margin: 0; font-size: 22px; line-height: 1.25; font-weight: 750; text-wrap: balance; }.ui-pwm-offer { margin: 0; color: var(--ui-muted); font-size: 16px; }.ui-pwm-offer b { color: var(--ui-fg); }.ui-pwm-list { display: grid; gap: 6px; margin: 4px 0; padding: 0; list-style: none; text-align: left; }.ui-pwm-list li { display: flex; gap: 8px; }.ui-pwm-list li::before { content: "✓"; color: var(--ui-success); font-weight: 800; }.ui-pwm-gate .ui-pwm-sub { height: 44px; min-width: 220px; font-size: 16px; }.ui-pwm-note { margin: 0; color: var(--ui-muted); font-size: 13px; }`// A price as it reads: "$1.00", "€4.99".fn paywall_meter_money(amount, code: str, locale: str) -> str {    let m = money.of(dec(amount), code, "half_up")    type_of(m) == "error" ? `${format_number(round(dec(amount), 2, "half_up"), locale, 2)} ${code}` : money.format(m, locale)}view PaywallMeter(used: int, limit: int, period: str = "this month", subscribe_href: str = "/subscribe", subscribe_label: str = "Subscribe",                  signin_href: str? = null, price = null, currency: str = "USD", price_period: str = "month", offer: str? = null,                  benefits: list = [], resets: str? = null, title: str? = null, locale: str = "en-US", id: str? = null, class: str? = null) {    style global paywall_meter_css    let read = used < 0 ? 0 : (used > limit ? limit : used)    let out = used > limit    let last = used == limit    let noun = (n: int) => n == 1 ? "free article" : "free articles"    let dots = <span.ui-pwm-dots role="img" aria-label={`${read} of ${limit} ${noun(limit)} read`}>        {for i in range(limit) => <span class={ui_cls(["ui-pwm-dot", i < read && "ui-pwm-dot-on"])}></span>}    </span>    let signin = signin_href != null && <a.ui-pwm-in href={signin_href}>Sign in</a>    out        ? <section class={ui_cls(["ui-pwm", "ui-pwm-gate", class])} id={id} aria-labelledby={id != null ? `${id}-title` : null}                   aria-label={id == null ? "Subscribe to keep reading" : null}>              {dots}              <h2.ui-pwm-title id={id != null ? `${id}-title` : null}>{title ?? `You've read all ${limit} ${noun(limit)} ${period}`}</h2>              <p.ui-pwm-offer>{offer ?? (price != null                  ? ["Subscribe for ", <b>{paywall_meter_money(price, currency, locale)} a {price_period}</b>, " to keep reading."]                  : "Subscribe to keep reading.")}</p>              {len(benefits) > 0 && <ul.ui-pwm-list>{for b in benefits => <li>{b}</li>}</ul>}              <a.ui-pwm-sub href={subscribe_href}>{subscribe_label}</a>              {signin_href != null && <p.ui-pwm-note>Already a subscriber? {signin}</p>}              {resets != null && <p.ui-pwm-note>{resets}</p>}          </section>        : <aside class={ui_cls(["ui-pwm", "ui-pwm-bar", last && "ui-pwm-last", class])} id={id} aria-label="Free articles">              {dots}              <p.ui-pwm-text>                  {last                      ? ["This is your ", <b>last {noun(1)}</b>, ` ${period}.`]                      : ["You have read ", <b>{read} of {limit}</b>, ` ${noun(limit)} ${period}.`]}              </p>              <div.ui-pwm-actions>                  {signin}                  <a.ui-pwm-sub href={subscribe_href}>{subscribe_label}</a>              </div>          </aside>}
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.