CurrencyConverter
An amount and two currencies with a swap button, the rate both ways and the converted amount, crossed through the base from the rates given.
Demo
Convert
Mid-market rates at 14:05 UTC, against USD. The rate you are offered may differ.
Trip budget
Mid-market rates at 14:05 UTC, against USD. The rate you are offered may differ.
Pick a trip budget.
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/currency_converter.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/currency_converter.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/currency_converter.qwe
let currency_converter_demo_rates = { USD: 1, EUR: 0.9224m, GBP: 0.7841m, JPY: 156.24m, CHF: 0.8812m, CAD: 1.3746m, PHP: 58.12m, MXN: 18.0450m }component CurrencyConverterDemoLive() { state note = "Pick a trip budget." <div.demo-stack> <CurrencyConverter name="trip" label="Trip budget" rates={currency_converter_demo_rates} amount="2500" from="USD" to="JPY" currencies={["USD", "EUR", "JPY", "PHP"]} updated="Mid-market rates at 14:05 UTC" on_change={(r) => { note = `${r.amount} ${r.from} is ${round(r.converted, 2)} ${r.to}.` }}/> <p.demo-note id="currency-converter-demo-note">{note}</p> </div>}view CurrencyConverterDemo() = <div.demo-stack> <CurrencyConverter rates={currency_converter_demo_rates} amount={1000} from="USD" to="EUR" updated="Mid-market rates at 14:05 UTC"/> <CurrencyConverterDemoLive/> </div>Source
ui/currency_converter.qwe
// CurrencyConverter: an amount and two currencies, with a button that swaps them,// the rate between the two both ways and the amount converted, worked out as it is// typed. The rates are given: how many of each currency one unit of the base buys// (the mid-market rates of your provider), so any pair is crossed through the base.// The fields are real (`amount`, `from`, `to`), so a plain <form> sends them.//// <CurrencyConverter rates={{ USD: 1, EUR: 0.9224m, GBP: 0.7841m, JPY: 156.24m }} amount={1000} from="USD" to="EUR"/>// <CurrencyConverter rates={rates} base="EUR" from="EUR" to="PLN" locale="de-DE" updated="Rates at 14:05 CET"// on_change={(r) => { total = r.converted }}/>//// rates: a map of ISO code to the units of it one `base` buys ({ USD: 1, EUR: 0.9224m });// decimals are best (numbers and numeric text are read as decimals). base: the code the// rates are against ("USD"), shown in the note. currencies: the codes to offer, in// order (all the rates' codes). amount: to start with (1000). from and to: the two// codes (the first two offered). names: the currencies' names for the lists ({ PHP:// "Philippine peso" }; the common ones are known). locale: how amounts are written// ("en-US"). updated: when the rates are from ("Mid-market rates at 14:05 UTC").// label: the heading ("Convert"; null for none). name: put before the fields' names// ("trip" makes trip_amount, trip_from, trip_to) and their ids. on_change(r): inside// one of your components, given { amount, from, to, rate, converted } (decimals) each// time they change, when the amount reads.import ui/theme.qwe use ui_clslet currency_converter_css = css`.ui-fxconv { display: grid; gap: 14px; min-width: 0; max-width: 580px; padding: 18px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-fxconv .ui-fxconv-title { margin: 0; font-size: 15px; font-weight: 650; }.ui-fxconv-fields { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) auto minmax(0, 1fr); align-items: end; gap: 10px; }.ui-fxconv-field { display: grid; gap: 6px; min-width: 0; }.ui-fxconv-label { font-size: 13px; font-weight: 500; color: var(--ui-muted); }.ui-fxconv-input, .ui-fxconv-select { box-sizing: border-box; width: 100%; height: 40px; margin: 0; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 15px var(--ui-font); font-variant-numeric: tabular-nums; transition: border-color .15s, box-shadow .15s; }.ui-fxconv-input { text-align: right; font-weight: 600; }.ui-fxconv-input:hover, .ui-fxconv-select:hover { border-color: var(--ui-strong); }.ui-fxconv-input:focus, .ui-fxconv-select:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-fxconv-input[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-fxconv-swap { display: grid; place-items: center; width: 40px; height: 40px; padding: 0; border: 1px solid var(--ui-border); border-radius: 50%; background: var(--ui-subtle); color: var(--ui-fg); cursor: pointer; transition: background .15s, color .15s, transform .2s; }.ui-fxconv-swap svg { width: 16px; height: 16px; }.ui-fxconv-swap:hover { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-fxconv-swap:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-fxconv-swap:active { transform: rotate(180deg); }.ui-fxconv-result { display: grid; gap: 4px; padding: 14px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-fxconv-from { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-fxconv-to { font-size: 26px; font-weight: 650; line-height: 1.2; letter-spacing: -.01em; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }.ui-fxconv-rates { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 4px 0 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-fxconv-error { margin: 0; color: var(--ui-danger); font-size: 13px; }.ui-fxconv-note { margin: 0; color: var(--ui-muted); font-size: 12px; }@media (max-width: 520px) { .ui-fxconv-fields { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); } .ui-fxconv-fields > :first-child { grid-column: 1 / -1; }}@media (prefers-reduced-motion: reduce) { .ui-fxconv-input, .ui-fxconv-select, .ui-fxconv-swap { transition: none; } }`let currency_converter_names = { USD: "US dollar", EUR: "Euro", GBP: "British pound", JPY: "Japanese yen", CHF: "Swiss franc", CAD: "Canadian dollar", AUD: "Australian dollar", NZD: "New Zealand dollar", CNY: "Chinese yuan", HKD: "Hong Kong dollar", SGD: "Singapore dollar", INR: "Indian rupee", PHP: "Philippine peso", MXN: "Mexican peso", BRL: "Brazilian real", ZAR: "South African rand", SEK: "Swedish krona", NOK: "Norwegian krone", DKK: "Danish krone", PLN: "Polish złoty", KRW: "South Korean won", THB: "Thai baht", IDR: "Indonesian rupiah", MYR: "Malaysian ringgit", AED: "UAE dirham", TRY: "Turkish lira", VND: "Vietnamese dong", NGN: "Nigerian naira" }// How many decimals a rate is written with: fewer for a large one (169.38), more// for a small one (0.006400).fn currency_converter_places(r: dec) -> int = r >= 100 ? 2 : (r >= 0.1m ? 4 : (r >= 0.001m ? 6 : 8))// An amount in its currency, rounded to its minor units: "$1,000.00", "¥156,240".fn currency_converter_money(amount: dec, code: str, locale: str) -> str { let m = money.of(amount, code, "half_up") type_of(m) == "error" ? `${format_number(round(amount, 2, "half_up"), locale, 2)} ${code}` : money.format(m, locale)}// What was typed as a decimal ("1,250.5" is 1250.5m), or null when it is not a number.fn currency_converter_read(text: str) { let t = re.replace(trim(text), "[,\\s_]", "") if t == "" { return null } let d = dec(t) is_err(d) || d < 0 ? null : d}component CurrencyConverter(rates: map, base: str = "USD", currencies: list? = null, amount = 1000, from: str? = null, to: str? = null, names: map? = null, locale: str = "en-US", updated: str? = null, label: str? = "Convert", name: str? = null, on_change = null, class: str? = null) { style global currency_converter_css let codes = currencies ?? keys(rates) state src = from ?? codes[0] state dst = to ?? (len(codes) > 1 ? codes[1] : codes[0]) state text = str(amount) let field = (f: str) => name != null ? `${name}_${f}` : f let id = `currency-converter-${name ?? "fx"}` let rate_of = (c: str) => rates[c] != null ? dec(rates[c]) : null let value = currency_converter_read(text) let a = rate_of(src) let b = rate_of(dst) let rate = a != null && b != null && a > 0 && b > 0 ? b / a : null let back = rate != null && rate > 0 ? 1 / rate : null let converted = value != null && rate != null ? value * rate : null let title = (c: str) => (names != null ? names[c] : null) ?? currency_converter_names[c] ?? c let tell = () => { let v = currency_converter_read(text) let ra = rate_of(src) let rb = rate_of(dst) if on_change != null && v != null && ra != null && rb != null && ra > 0 { on_change({ amount: v, from: src, to: dst, rate: rb / ra, converted: v * rb / ra }) } } let options = (picked: str) => for c in codes => <option key={c} value={c} selected={c == picked}>{c} · {title(c)}</option> <div class={ui_cls(["ui-fxconv", class])} role="group" aria-labelledby={label != null ? `${id}-title` : null} aria-label={label == null ? "Currency converter" : null}> {label != null && <h3.ui-fxconv-title id={`${id}-title`}>{label}</h3>} <div.ui-fxconv-fields> <div.ui-fxconv-field> <label.ui-fxconv-label for={`${id}-amount`}>Amount</label> <input.ui-fxconv-input type="text" id={`${id}-amount`} name={field("amount")} value={text} inputmode="decimal" autocomplete="off" aria-invalid={value == null ? "true" : null} aria-describedby={value == null ? `${id}-error` : null} @input={(e) => { text = e.value tell() }}/> </div> <div.ui-fxconv-field> <label.ui-fxconv-label for={`${id}-from`}>From</label> <select.ui-fxconv-select id={`${id}-from`} name={field("from")} @change={(e) => { src = e.value tell() }}>{options(src)}</select> </div> <button.ui-fxconv-swap type="button" aria-label={`Swap: convert ${dst} to ${src}`} title="Swap currencies" @click={() => { let s = src src = dst dst = s tell() }}> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M2 5h11M10 2l3 3-3 3M14 11H3M6 8l-3 3 3 3"/> </svg> </button> <div.ui-fxconv-field> <label.ui-fxconv-label for={`${id}-to`}>To</label> <select.ui-fxconv-select id={`${id}-to`} name={field("to")} @change={(e) => { dst = e.value tell() }}>{options(dst)}</select> </div> </div> <output.ui-fxconv-result for={`${id}-amount ${id}-from ${id}-to`} aria-live="polite"> {converted != null ? [<span.ui-fxconv-from>{currency_converter_money(value, src, locale)} =</span>, <span.ui-fxconv-to data-amount={str(round(converted, 8))}>{currency_converter_money(converted, dst, locale)}</span>] : <span.ui-fxconv-to>—</span>} {rate != null && <ul.ui-fxconv-rates> <li data-rate={str(round(rate, 8))}>1 {src} = {format_number(round(rate, currency_converter_places(rate), "half_up"), locale, currency_converter_places(rate))} {dst}</li> <li>1 {dst} = {format_number(round(back, currency_converter_places(back), "half_up"), locale, currency_converter_places(back))} {src}</li> </ul>} </output> {value == null && <p.ui-fxconv-error id={`${id}-error`} role="alert">Enter an amount, such as 1,250.50.</p>} {rate == null && <p.ui-fxconv-error role="alert">No rate for {src} or {dst}.</p>} <p.ui-fxconv-note>{updated ?? "Mid-market rates"}, against {base}. The rate you are offered may differ.</p> </div>}Related
RemittanceQuoteSending money abroad: you send, the fee, the rate against the mid-market rate, what they get and when it arrives, worked out as the amount changes.8 views · 5 downloadsFxQuoteTicketAn order ticket for a currency deal: buy or sell, the amount, the quoted rate counting down to its expiry, the other currency's amount and Confirm.8 views · 6 downloadsLoanCalculatorA loan's amount, rate and term as sliders and boxes, with the monthly payment, total interest and total paid following as they move.7 views · 5 downloadsLoanEligibilityFour questions (income, debts, work, amount) and, as they are answered, whether a loan is likely, the amount within reach and why.7 views · 6 downloadsPatronageRefundCalcWorks out a co-op member's dividend on shares and patronage refund on interest paid or purchases, to the total, as they type.7 views · 4 downloadsRateAlertsSet an alert on an exchange rate, a pair above or below a rate, and see the alerts set, how far each is, and remove them.8 views · 6 downloads
Thread
Thread 0 posts
On CurrencyConverter: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.