CurrencyWallet
Balances in several currencies with the total in a base currency, each with its rate, its worth and share, and convert and send actions.
Demo
Wallet · total in USD
$19,159.46
6 currencies · rates 14:05 UTC · 1 without a rate, left out
- USD US dollaraccount ending •••• 4821$12,480.5565.1% of the total
- EUR Euroaccount ending •••• 2046€3,250.00≈ $3,523.42
- JPY Japanese yen¥185,000≈ $1,184.08
- GBP British pound£840.20≈ $1,071.55
- PHP Philippine pesoaccount ending •••• 7310₱52,300.00≈ $899.86
- KES Kenyan shillingKES 15,000.00no rate
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_wallet.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/currency_wallet.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/currency_wallet.qwe
view CurrencyWalletDemo() = <div.demo-stack> <CurrencyWallet id="currency-wallet-demo" base="USD" updated="14:05 UTC" rates={{ USD: 1, EUR: 0.9224m, GBP: 0.7841m, JPY: 156.24m, PHP: 58.12m }} convert_href="#convert?from={currency}" send_href="#send?currency={currency}" balances={[ { currency: "PHP", amount: 52300m, account: "•••• 7310" }, { currency: "EUR", amount: 3250m, account: "•••• 2046" }, { currency: "USD", amount: 12480.55m, account: "•••• 4821" }, { currency: "JPY", amount: 185000 }, { currency: "GBP", amount: 840.20m }, { currency: "KES", amount: 15000m }, ]}/> <div.demo-narrow> <CurrencyWallet id="currency-wallet-demo-eur" label="Travel card" base="EUR" rates={{ EUR: 1, CHF: 0.9553m }} convert_href="#convert?from={currency}" send_href={null} keep_order={true} balances={[{ currency: "CHF", amount: 420m }, { currency: "EUR", amount: 185.40m }]}/> </div> </div>Source
ui/currency_wallet.qwe
// CurrencyWallet: money held in several currencies: the total in a base currency at// the top, then each balance with its flag, the amount in its own currency, its rate// to the base, what it is worth in the base and its share of the whole, and links to// convert it or send it. Balances in the base come first, then the largest.//// <CurrencyWallet base="USD" rates={{ USD: 1, EUR: 0.9224m, JPY: 156.24m }} balances={[// { currency: "USD", amount: 12480.55m, account: "•••• 4821" },// { currency: "EUR", amount: 3250m }, { currency: "JPY", amount: 185000 }]}/>//// balances: a list of { currency, amount, name, account }: the ISO code, the amount// (decimals best; numbers and numeric text are read as decimals), a name ("Euro"; the// common ones are known) and the account it is held in, masked ("•••• 4821"). base:// the currency of the total ("USD"). rates: a map of code to the units of it one base// buys ({ EUR: 0.9224m }); a balance without one shows "—" and is left out of the// total, which says so. convert_href and send_href: the links of each balance's// actions, with {currency} put in ("/convert?from={currency}", "/send?currency={currency}";// null drops the action). keep_order: show the balances in the order given (false).// locale: how amounts are written ("en-US"). label: the heading ("Wallet"). updated:// when the rates are from. id: the element's id.import ui/theme.qwe use ui_clslet currency_wallet_css = css`.ui-fxwallet { display: grid; gap: 0; min-width: 0; max-width: 620px; 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); font-variant-numeric: tabular-nums; overflow: hidden; }.ui-fxwallet-head { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 12px; padding: 18px 18px 16px; border-bottom: 1px solid var(--ui-border); }.ui-fxwallet .ui-fxwallet-title { margin: 0; color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-fxwallet-total { margin: 2px 0 0; font-size: 30px; font-weight: 700; line-height: 1.15; letter-spacing: -.01em; }.ui-fxwallet-sub { margin: 2px 0 0; color: var(--ui-muted); font-size: 12px; }.ui-fxwallet-actions { display: flex; gap: 8px; }.ui-fxwallet-btn { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: 500 13px var(--ui-font); text-decoration: none; }.ui-fxwallet-btn svg { width: 14px; height: 14px; }.ui-fxwallet-btn:hover { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-fxwallet-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-fxwallet-primary { border-color: var(--ui-accent); background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-fxwallet-primary:hover { color: var(--ui-accent-fg); filter: brightness(1.08); }.ui-fxwallet-list { margin: 0; padding: 0; list-style: none; }.ui-fxwallet-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 4px 12px; padding: 12px 18px; }.ui-fxwallet-item:not(:last-child) { border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 60%, transparent); }.ui-fxwallet-flag { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: var(--ui-subtle); font-size: 18px; }.ui-fxwallet-flag small { font-size: 10px; font-weight: 700; color: var(--ui-muted); }.ui-fxwallet-name b { font-weight: 650; }.ui-fxwallet-name small { display: block; color: var(--ui-muted); font-size: 12px; }.ui-fxwallet-amount { text-align: right; }.ui-fxwallet-amount b { display: block; font-size: 15px; font-weight: 650; }.ui-fxwallet-amount small { color: var(--ui-muted); font-size: 12px; }.ui-fxwallet-foot { grid-column: 2 / -1; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 14px; }.ui-fxwallet-share { flex: 1; min-width: 80px; max-width: 220px; height: 4px; border-radius: 2px; background: var(--ui-subtle); overflow: hidden; }.ui-fxwallet-share span { display: block; height: 100%; border-radius: 2px; background: var(--ui-accent); }.ui-fxwallet-links { display: flex; gap: 12px; font-size: 13px; }.ui-fxwallet-links a { color: var(--ui-accent); font-weight: 500; text-decoration: none; }.ui-fxwallet-links a:hover { text-decoration: underline; }.ui-fxwallet-links a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-fxwallet-none { color: var(--ui-muted); }.ui-fxwallet-rate { color: var(--ui-muted); font-size: 12px; }`let currency_wallet_flags = { USD: "🇺🇸", EUR: "🇪🇺", GBP: "🇬🇧", JPY: "🇯🇵", CHF: "🇨🇭", CAD: "🇨🇦", AUD: "🇦🇺", NZD: "🇳🇿", CNY: "🇨🇳", HKD: "🇭🇰", SGD: "🇸🇬", INR: "🇮🇳", PHP: "🇵🇭", MXN: "🇲🇽", BRL: "🇧🇷", ZAR: "🇿🇦", SEK: "🇸🇪", NOK: "🇳🇴", DKK: "🇩🇰", PLN: "🇵🇱", KRW: "🇰🇷", THB: "🇹🇭", IDR: "🇮🇩", MYR: "🇲🇾", AED: "🇦🇪", TRY: "🇹🇷", KES: "🇰🇪", NGN: "🇳🇬", VND: "🇻🇳" }let currency_wallet_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", KES: "Kenyan shilling", AED: "UAE dirham", ZAR: "South African rand" }// An amount in its currency, to its minor units: "$12,480.55", "¥185,000".fn currency_wallet_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)}// An amount rounded to the currency's minor units (2 when it is not known).fn currency_wallet_round(amount: dec, code: str) -> dec { let m = money.of(amount, code, "half_up") type_of(m) == "error" ? round(amount, 2, "half_up") : m.amount}// The decimals a rate is written with: fewer for a large one (169.38), more for a small one (0.006400).fn currency_wallet_places(r: dec) -> int = r >= 100 ? 2 : (r >= 0.1m ? 4 : (r >= 0.001m ? 6 : 8))view CurrencyWallet(balances: list = [], base: str = "USD", rates: map = {}, convert_href: str? = "/convert?from={currency}", send_href: str? = "/send?currency={currency}", keep_order: bool = false, locale: str = "en-US", label: str = "Wallet", updated: str? = null, id: str? = null, class: str? = null) { style global currency_wallet_css let home = upper(base) let rows = balances | map((b) => { let code = upper(b.currency) let amount = dec(b.amount) let r = code == home ? 1m : (rates[code] != null ? dec(rates[code]) : null) let worth = r != null && r > 0 ? currency_wallet_round(amount / r, home) : null { ...b, code, amount, r, worth } }) let known = rows | filter((r) => r.worth != null) let total = sum(known | map((r) => r.worth)) let ordered = keep_order ? rows : (rows | filter((r) => r.code == home)) + (rows | filter((r) => r.code != home) | sort_by((r) => r.worth == null ? 1 : -r.worth)) let link = (tpl: str?, code: str) => tpl != null ? replace(tpl, "{currency}", code) : null let missing = len(rows) - len(known) <section class={ui_cls(["ui-fxwallet", class])} id={id} aria-label={label}> <div.ui-fxwallet-head> <div> <h3.ui-fxwallet-title>{label} · total in {home}</h3> <p.ui-fxwallet-total data-total={str(total)}>{currency_wallet_money(dec(total), home, locale)}</p> <p.ui-fxwallet-sub>{len(rows)} {len(rows) == 1 ? "currency" : "currencies"}{updated != null ? ` · rates ${updated}` : ""}{missing > 0 ? ` · ${missing} without a rate, left out` : ""}</p> </div> <div.ui-fxwallet-actions> {convert_href != null && <a.ui-fxwallet-btn href={link(convert_href, home)}> <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.5 4.5h10M9 2l2.5 2.5L9 7M12.5 9.5h-10M5 7L2.5 9.5 5 12"/></svg>Convert</a>} {send_href != null && <a class="ui-fxwallet-btn ui-fxwallet-primary" href={link(send_href, home)}> <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.5 1.5L6 8M12.5 1.5L8.5 12.5 6 8 1.5 5.5z"/></svg>Send</a>} </div> </div> <ul.ui-fxwallet-list> {for b in ordered { let flag = currency_wallet_flags[b.code] let title = get(b, "name") ?? currency_wallet_names[b.code] ?? b.code let share = b.worth != null && total > 0 ? float(b.worth) * 100 / float(total) : 0 <li key={b.code + (get(b, "account") ?? "")} class="ui-fxwallet-item" data-code={b.code}> <span.ui-fxwallet-flag aria-hidden="true">{flag != null ? flag : <small>{b.code}</small>}</span> <span.ui-fxwallet-name><b>{b.code}</b> {title} {get(b, "account") != null && <small><span.ui-sr>account ending </span>{b.account}</small>}</span> <span.ui-fxwallet-amount> <b data-col="amount">{currency_wallet_money(b.amount, b.code, locale)}</b> <small data-col="worth">{b.code == home ? (len(rows) > 1 ? `${format_number(round(share, 1), locale, 1)}% of the total` : "") : (b.worth != null ? `≈ ${currency_wallet_money(b.worth, home, locale)}` : <span.ui-fxwallet-none>no rate</span>)}</small> </span> <div.ui-fxwallet-foot> <small.ui-fxwallet-rate data-col="rate">{b.code == home ? "Base currency" : (b.r != null ? `1 ${b.code} = ${format_number(round(1 / b.r, currency_wallet_places(1 / b.r), "half_up"), locale, currency_wallet_places(1 / b.r))} ${home}` : "—")}</small> <span.ui-fxwallet-share role="img" aria-label={`${format_number(round(share, 1), locale, 1)}% of the total`}><span style={`width: ${round(share * 100) / 100}%`}></span></span> {(convert_href != null || send_href != null) && <span.ui-fxwallet-links> {convert_href != null && <a href={link(convert_href, b.code)} aria-label={`Convert ${b.code}`}>Convert</a>} {send_href != null && <a href={link(send_href, b.code)} aria-label={`Send ${b.code}`}>Send</a>} </span>} </div> </li> }} </ul> </section>}Related
AccountCardA bank account at a glance: type, nickname, masked number, current and available balance, what is on hold, and quick actions.8 views · 8 downloadsAccountDetailsThe details to share to get paid: account name and number, IBAN in groups of four (checked), BIC, sort code or routing number, each with a copy button.6 views · 6 downloadsCardControlsA card's controls beside its masked face: freeze it, set daily spending and cash limits, and allow online, ATM, contactless and abroad use.7 views · 6 downloadsCurrencyPairCardOne currency pair: the bid and ask large with the pip digits biggest, the spread, the day's change, low and high, and a sparkline of the day.7 views · 7 downloadsLoanSummaryA loan in a card: the outstanding balance out of the amount borrowed, how much is paid off as a bar, the rate, and the next payment and when it is due.8 views · 8 downloadsRemittanceQuoteSending 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 · 6 downloads
Thread
Thread 0 posts
On CurrencyWallet: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.