Finance

LoanOffers

Loan offers side by side: each lender's APR, term, monthly payment, fees and total cost, with the cheapest in total marked.

#finance#loans#card#data#no-js

Demo

Preview
  • Harbor Federal

    7.49%APR

    Monthly
    $400.66
    Term
    60 months (5 years)
    Fees
    None
    Interest
    $4,039.60
    Total cost
    $24,039.60

    $1,023.20 more than the cheapest

    No early repayment fee.

    Choose Harbor Federal
  • Summit Lending

    Pre-approved

    6.99%APR

    Monthly
    $395.93
    Term
    60 months (5 years)
    Fees
    $600.00
    Interest
    $3,755.80
    Total cost
    $24,355.80

    $1,339.40 more than the cheapest

    Choose Summit Lending
  • Bluewater Bank

    8.25%APR

    Monthly
    $353.11
    Term
    72 months (6 years)
    Fees
    $150.00
    Interest
    $5,423.92
    Total cost
    $25,573.92

    $2,557.52 more than the cheapest

    Lowest monthly payment.

    Choose Bluewater Bank
  • Lowest total cost

    Northwind Credit Union

    Members

    6.75%APR

    Monthly
    $474.30
    Term
    48 months (4 years)
    Fees
    $250.00
    Interest
    $2,766.40
    Total cost
    $23,016.40
  • Lowest total cost

    Rheinland Bank

    5.9%APR

    Monthly
    364,52 €
    Term
    36 months (3 years)
    Fees
    None
    Interest
    1.122,72 €
    Total cost
    13.122,72 €
  • Alpen Kredit

    5.4%APR

    Monthly
    361,81 €
    Term
    36 months (3 years)
    Fees
    240,00 €
    Interest
    1.025,16 €
    Total cost
    13.265,16 €

    142,44 € more than the cheapest

    Auswählen Alpen Kredit

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

Example

The code of the preview above.

demos/loan_offers.qwe
view LoanOffersDemo() {    <div.demo-stack style="padding-top: 10px">        <LoanOffers id="loan-offers-demo-personal" amount={20000} offers={[            { lender: "Harbor Federal", apr: 7.49, term: 60, fees: 0, href: "#harbor", note: "No early repayment fee." },            { lender: "Summit Lending", apr: 6.99, term: 60, fees: 600, tag: "Pre-approved", href: "#summit" },            { lender: "Bluewater Bank", apr: 8.25, term: 72, fees: 150, href: "#bluewater", note: "Lowest monthly payment." },            { lender: "Northwind Credit Union", apr: 6.75, rate: 6.5, term: 48, fees: 250, tag: "Members", href: "#northwind" },        ]}/>        <div.demo-narrow style="max-width: 520px">            <LoanOffers id="loan-offers-demo-eur" amount={12000} currency="EUR" locale="de-DE" choose_label="Auswählen" offers={[                { lender: "Rheinland Bank", apr: 5.9, term: 36, href: "#rheinland" },                { lender: "Alpen Kredit", apr: 5.4, term: 36, fees: 240, monthly: 361.81, href: "#alpen" },            ]}/>        </div>    </div>}

Source

ui/loan_offers.qwe
// LoanOffers: loan offers side by side, for one amount: each lender's APR, term,// monthly payment, fees and what the loan costs in all, the cheapest in total// marked ("Lowest total cost"), and how much more each other one costs. The// monthly payment is worked out from the rate when an offer does not give it// (the annuity formula, rounded half up to the cent), in decimals.////   <LoanOffers amount={20000} offers={[//       { lender: "Harbor Federal", apr: 7.49, term: 60, fees: 0, href: "/offers/hf" },//       { lender: "Summit Lending", apr: 6.99, term: 60, fees: 600, tag: "Pre-approved" },//       { lender: "Bluewater Bank", apr: 8.25, term: 72, fees: 150, monthly: 353.42 }]}/>//// amount: what is borrowed, for every offer (a number or a decimal). offers: a list// of { lender, apr, term, fees?, rate?, monthly?, amount?, tag?, note?, href? }: apr// the yearly rate in percent shown; rate the one to work the payment out with (the// APR by default); term in months; fees paid on top (0); monthly the payment, when// the lender gives it; amount another amount for this offer; tag a badge// ("Pre-approved"); note a line at the bottom; href a link to take it (choose_label,// "Choose"). The total cost is the monthly payment times the term, plus the fees.// currency: ISO code ("USD"); locale: how amounts are written (the program's;// en-US). label: what screen readers call the list ("Loan offers"). id, class.//// loan_offers_cost(offer, amount) gives an offer's { monthly, interest, fees, total }.import ui/theme.qwe use ui_clslet loan_offers_css = css`.ui-loan-offers { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px;  min-width: 0; color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-loan-offers-offer { position: relative; display: grid; grid-template-rows: auto auto 1fr auto; gap: 12px; height: 100%; box-sizing: border-box;  padding: 18px 18px 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); }.ui-loan-offers-best { border-color: var(--ui-success); box-shadow: 0 0 0 1px var(--ui-success); }.ui-loan-offers-flag { position: absolute; top: -11px; left: 14px; padding: 2px 10px; border-radius: 999px; background: var(--ui-success); color: #fff;  font-size: 11px; font-weight: 700; letter-spacing: .03em; }.ui-loan-offers-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }.ui-loan-offers-lender { margin: 0; font-size: 15px; font-weight: 650; line-height: 1.3; }.ui-loan-offers-tag { flex: none; padding: 1px 8px; border-radius: 999px; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 11px; font-weight: 600; }.ui-loan-offers-apr { margin: 0; font-size: 28px; font-weight: 700; letter-spacing: -.01em; line-height: 1.1; font-variant-numeric: tabular-nums; }.ui-loan-offers-apr small { margin-left: 4px; color: var(--ui-muted); font-size: 13px; font-weight: 500; letter-spacing: 0; }.ui-loan-offers-facts { display: grid; grid-template-columns: 1fr auto; gap: 7px 12px; margin: 0; align-content: start; }.ui-loan-offers-facts dt { color: var(--ui-muted); }.ui-loan-offers-facts dd { margin: 0; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-loan-offers-facts .ui-loan-offers-total { padding-top: 8px; border-top: 1px solid var(--ui-border); color: var(--ui-fg); font-weight: 700; }.ui-loan-offers-best .ui-loan-offers-total { color: var(--ui-success); }.ui-loan-offers-foot { display: grid; gap: 8px; }.ui-loan-offers-diff { margin: 0; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-loan-offers-note { margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-loan-offers-choose { display: inline-flex; align-items: center; justify-content: center; height: 34px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font-weight: 600; text-decoration: none; }.ui-loan-offers-choose:hover { background: var(--ui-subtle); }.ui-loan-offers-best .ui-loan-offers-choose { border-color: transparent; background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-loan-offers-best .ui-loan-offers-choose:hover { background: var(--ui-accent); filter: brightness(1.08); }.ui-loan-offers-choose:focus-visible { outline: none; box-shadow: var(--ui-focus); }`// x to the power n, for a whole n, exactly (pow takes no decimals).fn loan_offers_pow(x: dec, n: int) -> dec {    let out = 1m    let i = 0    while i < n {        out = out * x        i += 1    }    out}// An offer's { monthly, interest, fees, total }, for amount unless it has its own.fn loan_offers_cost(offer: map, amount) -> map {    let principal = dec(offer.amount ?? amount)    let months = int(offer.term)    let rate = dec(offer.rate ?? offer.apr)    let monthly = if offer.monthly != null {        dec(offer.monthly)    } else if rate <= 0 {        round(principal / months, 2, "ceiling")    } else {        let r = rate / 1200        let f = loan_offers_pow(1m + r, months)        round(principal * r * f / (f - 1m), 2, "half_up")    }    let fees = dec(offer.fees ?? 0)    let repaid = monthly * months    { monthly, interest: repaid - principal, fees, total: repaid + fees }}fn loan_offers_money(x: dec, currency: str, locale: str?) -> str {    let m = money.of(x, currency, "half_up")    if is_err(m) { return str(x) }    locale != null ? money.format(m, locale) : money.format(m)}// A term in months, as people say it: "60 months (5 years)".fn loan_offers_term(months: int) -> str =    months % 12 == 0 && months >= 12 ? `${months} months (${int(months / 12)} ${months == 12 ? "year" : "years"})` : `${months} months`view LoanOffers(offers: list, amount = null, currency: str = "USD", locale: str? = null, choose_label: str = "Choose",                label: str = "Loan offers", id: str? = null, class: str? = null) {    style global loan_offers_css    let costed = for o in offers => merge(o, { cost: loan_offers_cost(o, amount) })    let cheapest = len(costed) > 0 ? min(for o in costed => o.cost.total) : 0m    let fmt = (x: dec) => loan_offers_money(x, currency, locale)    <ul class={ui_cls(["ui-loan-offers", class])} id={id} aria-label={label}>        {for [i, o] in enumerate(costed) {            let best = len(costed) > 1 && o.cost.total == cheapest            let heading = id != null ? `${id}-${i}` : null            <li key={i}>                <article class={ui_cls(["ui-loan-offers-offer", best && "ui-loan-offers-best"])} aria-labelledby={heading} data-best={best ? "true" : null}>                    {best && <span.ui-loan-offers-flag>Lowest total cost</span>}                    <div.ui-loan-offers-head>                        <h3.ui-loan-offers-lender id={heading}>{o.lender}</h3>                        {o.tag != null && <span.ui-loan-offers-tag>{o.tag}</span>}                    </div>                    <p.ui-loan-offers-apr data-key="apr">{dec(o.apr)}%<small>APR</small></p>                    <dl.ui-loan-offers-facts>                        <dt>Monthly</dt><dd data-key="monthly">{fmt(o.cost.monthly)}</dd>                        <dt>Term</dt><dd data-key="term">{loan_offers_term(int(o.term))}</dd>                        <dt>Fees</dt><dd data-key="fees">{o.cost.fees > 0 ? fmt(o.cost.fees) : "None"}</dd>                        <dt>Interest</dt><dd data-key="interest">{fmt(o.cost.interest)}</dd>                        <dt.ui-loan-offers-total>Total cost</dt><dd.ui-loan-offers-total data-key="total">{fmt(o.cost.total)}</dd>                    </dl>                    <div.ui-loan-offers-foot>                        {!best && len(costed) > 1 && <p.ui-loan-offers-diff data-key="diff">{fmt(o.cost.total - cheapest)} more than the cheapest</p>}                        {o.note != null && <p.ui-loan-offers-note>{o.note}</p>}                        {o.href != null && <a.ui-loan-offers-choose href={o.href}>{choose_label}<span.ui-sr> {o.lender}</span></a>}                    </div>                </article>            </li>        }}    </ul>}
Thread
87Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.