AccountDetails
The 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.
Demo
Account details
Fernway Credit Union, 40 Quay Street, Port Ellis
- Account name
- Dana Whitfield
- Account number
- 40031204
- Sort code
- 60-14-22
- IBAN
- GB98 FERN 6014 2240 0312 04
- BIC / SWIFT
- FERNGB2P
- Currency
- GBP
Get paid by direct deposit
Harbor Bank
- Account name
- Northwind Ltd
- Account number
- 4400 1234 5678
- Routing number (ABA)
- 011000015
- Reference
- INV-0042
Check before sharing
- Account name
- Tomás Reyes
- IBAN
- GB99 FERN 6014 2240 0312 04The check digits do not match: check this IBAN.
- BIC / SWIFT
- FERNGB2P
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/account_details.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/account_details.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/account_details.qwe
view AccountDetailsDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px"> <AccountDetails id="account-details-demo" name="Dana Whitfield" bank="Fernway Credit Union" bank_address="40 Quay Street, Port Ellis" number="40031204" sort_code="601422" iban="GB98FERN60142240031204" bic="fernGB2p" currency="GBP"/> <AccountDetails id="account-details-demo-us" title="Get paid by direct deposit" name="Northwind Ltd" bank="Harbor Bank" number="4400 1234 5678" routing="011000015" reference="INV-0042"/> <AccountDetails id="account-details-demo-typo" title="Check before sharing" name="Tomás Reyes" iban="GB99 FERN 6014 2240 0312 04" bic="FERNGB2P" copy_all={false}/> </div>Source
ui/account_details.qwe
// AccountDetails: what to give someone so they can pay you: the account's name// and number, the IBAN in groups of four, the BIC/SWIFT, the sort code or the// routing number, each with a button that copies it (the IBAN without its// spaces), and one that copies them all. An IBAN whose check digits do not add// up is flagged, so a typo is caught before it is shared.//// <AccountDetails name="Dana Whitfield" bank="Fernway Credit Union" number="40031204" sort_code="601422"// iban="GB98FERN60142240031204" bic="FERNGB2P" currency="GBP"/>// <AccountDetails name="Northwind Ltd" number="4400 1234 5678" routing="011000015" bank="Harbor Bank"/>//// name: the account holder's name, as the payer must write it. number: the// account number. iban: shown grouped by four ("GB98 FERN 6014 ..."), copied// without spaces; checked by its check digits (mod 97). bic: the BIC or SWIFT// code. sort_code: six digits, shown as "60-14-22". routing: an ABA routing// number. bank, bank_address: the bank, shown and copied with the rest. currency:// the account's currency, a line of its own. reference: what the payer should// write as the reference. title: the heading ("Account details"; null for none).// copy_all: the Copy all button (true). id: the element's id.import ui/theme.qwe use ui_clslet account_details_css = css`.ui-acdet { display: grid; gap: 12px; align-content: start; min-width: 0; max-width: 520px; padding: 18px; 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); }.ui-acdet-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; }.ui-acdet-title { margin: 0; font-size: 16px; font-weight: 600; }.ui-acdet-bank { margin: 2px 0 0; color: var(--ui-muted); font-size: 13px; }.ui-acdet-list { margin: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); }.ui-acdet-row { display: flex; align-items: center; gap: 12px; padding: 9px 8px 9px 12px; }.ui-acdet-row + .ui-acdet-row { border-top: 1px solid var(--ui-border); }.ui-acdet-row > div { display: grid; flex: 1; gap: 1px; min-width: 0; }.ui-acdet-row dt { color: var(--ui-muted); font-size: 12px; }.ui-acdet-row dd { margin: 0; font-weight: 500; overflow-wrap: anywhere; }.ui-acdet-code dd { font-family: var(--ui-mono); font-size: 14px; letter-spacing: .03em; }.ui-acdet-bad dd { color: var(--ui-danger); }.ui-acdet-warn { display: block; color: var(--ui-danger); font-family: var(--ui-font); font-size: 12px; font-weight: 500; letter-spacing: 0; }.ui-acdet-copy { flex: none; display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 12px var(--ui-font); cursor: pointer; transition: border-color .15s, color .15s; }.ui-acdet-copy:hover { border-color: var(--ui-strong); color: var(--ui-accent); }.ui-acdet-copy:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-acdet-copy svg { width: 13px; height: 13px; }.ui-acdet-ok { border-color: var(--ui-success); color: var(--ui-success); }.ui-acdet-fail { border-color: var(--ui-danger); color: var(--ui-danger); }.ui-acdet-all { height: 32px; padding: 0 12px; background: var(--ui-subtle); }@media (prefers-reduced-motion: reduce) { .ui-acdet-copy { transition: none; } }`let account_details_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"// An IBAN without spaces, in capitals.fn account_details_compact(iban: str) -> str = upper(re.replace(iban, "[^0-9A-Za-z]", ""))// An IBAN in groups of four: "GB98 FERN 6014 2240 0312 04".fn account_details_grouped(iban: str) -> str { let c = account_details_compact(iban) join(range(ceil(len(c) / 4)) | map((i) => slice(c, i * 4, i * 4 + 4)), " ")}// Whether an IBAN's check digits add up (ISO 13616: mod 97 of the moved, numbered letters is 1).fn account_details_iban_ok(iban: str) -> bool { let c = account_details_compact(iban) if len(c) < 15 || len(c) > 34 || !re.test(c, "^[A-Z]{2}[0-9]{2}[A-Z0-9]+$") { return false } let moved = split(slice(c, 4) + slice(c, 0, 4), "") let r = reduce(moved, 0, (acc, ch) => { let letter = index_of(account_details_letters, ch) letter != null ? (acc * 100 + letter + 10) % 97 : (acc * 10 + int(ch)) % 97 }) r == 1}// A sort code as "60-14-22", from six digits written any way.fn account_details_sort_code(code: str) -> str { let d = re.replace(code, "[^0-9]", "") len(d) == 6 ? `${slice(d, 0, 2)}-${slice(d, 2, 4)}-${slice(d, 4, 6)}` : code}component AccountDetails(name: str? = null, number: str? = null, iban: str? = null, bic: str? = null, sort_code: str? = null, routing: str? = null, bank: str? = null, bank_address: str? = null, currency: str? = null, reference: str? = null, title: str? = "Account details", copy_all: bool = true, id: str? = null, class: str? = null) { style global account_details_css state copied = "" // the key of the row just copied, "all", or "" state result = "" // "ok" or "bad" if copied != "" { every(1800, () => { copied = "" }) } let iban_ok = iban == null || account_details_iban_ok(iban) let rows = [ name != null && { key: "name", label: "Account name", what: "account name", shown: name, copy: name, code: false }, number != null && { key: "number", label: "Account number", what: "account number", shown: number, copy: re.replace(number, "\\s", ""), code: true }, sort_code != null && { key: "sort_code", label: "Sort code", what: "sort code", shown: account_details_sort_code(sort_code), copy: account_details_sort_code(sort_code), code: true }, routing != null && { key: "routing", label: "Routing number (ABA)", what: "routing number", shown: routing, copy: re.replace(routing, "\\s", ""), code: true }, iban != null && { key: "iban", label: "IBAN", what: "IBAN", shown: account_details_grouped(iban), copy: account_details_compact(iban), code: true }, bic != null && { key: "bic", label: "BIC / SWIFT", what: "BIC", shown: upper(bic), copy: upper(re.replace(bic, "\\s", "")), code: true }, reference != null && { key: "reference", label: "Reference", what: "reference", shown: reference, copy: reference, code: false }, currency != null && { key: "currency", label: "Currency", what: "currency", shown: upper(currency), copy: upper(currency), code: true }, ] | filter((r) => r != false) let all_text = join((bank != null ? [`Bank: ${bank}${bank_address != null ? ", " + bank_address : ""}`] : []) + (rows | map((r) => `${r.label}: ${r.shown}`)), "\n") let copy = (key: str, text: str) => { copied = key if js("isSecureContext") != true { result = "bad" } else { // The promise's callbacks are given undefined, which is no argument: hence `= null`. js("navigator.clipboard.writeText", text).then((_ = null) => { result = "ok" }, (_ = null) => { result = "bad" }) } } let said = (key: str) => copied == key ? (result == "ok" ? "Copied" : result == "bad" ? "Could not copy" : "Copy") : "Copy" let state_class = (key: str) => copied == key && result != "" ? `ui-acdet-${result == "ok" ? "ok" : "fail"}` : null let base = id ?? "account-details" <section class={ui_cls(["ui-acdet", class])} id={id} aria-labelledby={title != null ? `${base}-title` : null} aria-label={title == null ? "Account details" : null}> {(title != null || bank != null || copy_all) && <div.ui-acdet-top> <div> {title != null && <h3.ui-acdet-title id={`${base}-title`}>{title}</h3>} {bank != null && <p.ui-acdet-bank>{bank}{bank_address != null && `, ${bank_address}`}</p>} </div> {copy_all && <button class={ui_cls(["ui-acdet-copy", "ui-acdet-all", state_class("all")])} type="button" data-copy={all_text} @click={() => copy("all", all_text)}>{said("all") == "Copy" ? "Copy all" : said("all")}</button>} </div>} <dl.ui-acdet-list> {for r in rows => <div class={ui_cls(["ui-acdet-row", r.code && "ui-acdet-code", r.key == "iban" && !iban_ok && "ui-acdet-bad"])} key={r.key} data-row={r.key}> <div> <dt id={`${base}-${r.key}`}>{r.label}</dt> <dd>{r.shown}{r.key == "iban" && !iban_ok && <span.ui-acdet-warn>The check digits do not match: check this IBAN.</span>}</dd> </div> <button class={ui_cls(["ui-acdet-copy", state_class(r.key)])} type="button" data-copy={r.copy} aria-label={`${said(r.key)} ${r.what}`} @click={() => copy(r.key, r.copy)}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> {copied == r.key && result == "ok" ? <path d="M20 6 9 17l-5-5"/> : <path d="M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2ZM4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"/>} </svg> <span aria-hidden="true">{said(r.key)}</span> </button> </div>} </dl> <p.ui-sr role="status">{copied != "" && result == "ok" ? "Copied" : copied != "" && result == "bad" ? "Could not copy" : ""}</p> </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 · 7 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 downloadsCurrencyWalletBalances in several currencies with the total in a base currency, each with its rate, its worth and share, and convert and send actions.9 views · 6 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 · 7 downloadsSavingsGoalA savings goal: target and saved so far, the progress, the deposit needed each month to reach it by the date, and the last deposits.7 views · 6 downloadsAccountPickerChoose the account to pay from as radio cards with each one's masked number and available balance, those short of the amount marked.7 views · 7 downloads
Thread
Thread 0 posts
On AccountDetails: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.