diff options
| author | Jurjen de Vries <[email protected]> | 2025-09-11 00:48:15 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-09-11 00:48:15 +0200 |
| commit | 0c69c9848c6b98c92609d209103efc60003c9723 (patch) | |
| tree | 445121def885f13d0f80c06864ad2ff4d5ef95a1 /src/components | |
| parent | 2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff) | |
| parent | 9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff) | |
| download | coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip | |
Merge branch 'coinos:master' into master
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/Account.svelte | 47 | ||||
| -rw-r--r-- | src/components/Amount.svelte | 16 | ||||
| -rw-r--r-- | src/components/AppHeader.svelte | 2 | ||||
| -rw-r--r-- | src/components/Balance.svelte | 29 | ||||
| -rw-r--r-- | src/components/Footer.svelte | 2 | ||||
| -rw-r--r-- | src/components/InvoiceActions.svelte | 113 | ||||
| -rw-r--r-- | src/components/Menu.svelte | 14 | ||||
| -rw-r--r-- | src/components/Nostr.svelte | 9 | ||||
| -rw-r--r-- | src/components/Numpad.svelte | 414 | ||||
| -rw-r--r-- | src/components/Payments.svelte | 11 | ||||
| -rw-r--r-- | src/components/Pin.svelte | 8 | ||||
| -rw-r--r-- | src/components/Qr.svelte | 14 | ||||
| -rw-r--r-- | src/components/SetAmount.svelte | 10 |
13 files changed, 403 insertions, 286 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte index 4edd659c..744821a3 100644 --- a/src/components/Account.svelte +++ b/src/components/Account.svelte @@ -23,36 +23,23 @@ aria-label="Payments" > <div class="flex"> - <Balance {balance} {user} {rate} /> - <a - href={`/payments`} - class="contents" - onclick={setAccount} - aria-label="Payments" - > - <button class="ml-auto flex gap-1 mb-auto pb-4 pl-4"> - <iconify-icon - noobserver - icon="ph:clock-bold" - width="32" - aria-label="Payments" - ></iconify-icon> - </button> - </a> - <a - href={id === user.id ? "/settings/nostr" : `/account/${id}`} - class="contents" - aria-label="Settings" - > - <button class="flex gap-1 mb-auto pb-4 pl-4"> - <iconify-icon - noobserver - icon="ph:gear-bold" - width="32" - aria-label="Settings" - ></iconify-icon> - </button> - </a> + <Balance {balance} {user} {rate} {id} /> + {#if id !== user.id} + <a + href={id === user.id ? "/settings/nostr" : `/account/${id}`} + class="contents" + aria-label="Settings" + > + <button class="flex gap-1 mb-auto pb-4 pl-4 ml-auto"> + <iconify-icon + noobserver + icon="ph:gear-bold" + width="32" + aria-label="Settings" + ></iconify-icon> + </button> + </a> + {/if} </div> <div class="flex justify-center w-full text-xl gap-2"> diff --git a/src/components/Amount.svelte b/src/components/Amount.svelte index f13949c6..2df2f702 100644 --- a/src/components/Amount.svelte +++ b/src/components/Amount.svelte @@ -1,7 +1,17 @@ <script> import { copy, s, f, toFiat } from "$lib/utils"; import { fiat } from "$lib/store"; - const { align, amount, locale, tip, rate, currency } = $props(); + const { + align, + amount, + locale, + tip, + rate, + currency, + locked = false, + } = $props(); + + const icon = $derived(locked ? "ph:lock-fill" : "ph:lightning-fill"); </script> {#if typeof amount !== "undefined"} @@ -11,14 +21,14 @@ class:justify-center={align !== "left"} > <div class="flex items-center" onclick={() => copy(amount)}> - <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300" + <iconify-icon noobserver {icon} class="text-yellow-300" ></iconify-icon>{s(amount, locale)} </div> {#if tip} <div class="flex items-center text-lg ml-2"> <div>+</div> - <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300" + <iconify-icon noobserver {icon} class="text-yellow-300" ></iconify-icon> <div>{s(tip, locale)}</div> </div> diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte index 7ed4563f..326f488b 100644 --- a/src/components/AppHeader.svelte +++ b/src/components/AppHeader.svelte @@ -93,7 +93,7 @@ <style> .btn-menu { - @apply flex justify-center items-center bg-base-100 p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl; + @apply flex justify-center items-center bg-base-100 p-2 rounded-full w-12 h-12 drop-shadow-xl; } header { diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte index da26f6de..dfa5f616 100644 --- a/src/components/Balance.svelte +++ b/src/components/Balance.svelte @@ -6,8 +6,8 @@ import { t } from "$lib/translations"; let show = $state(); - let { user, rate, balance } = $props(); - let { currency } = $derived(user); + let { id, user, rate, balance } = $props(); + let { currency, locked } = $derived(user); let locale = loc(user); let toggleShow = (e) => { e.preventDefault(); @@ -23,11 +23,32 @@ <div> {#if user.haspin && !$pin} <button onclick={toggleShow} class="flex gap-2 text-xl"> - <iconify-icon noobserver icon="ph:eye-slash-bold" width="32"></iconify-icon> + <iconify-icon noobserver icon="ph:eye-slash-bold" width="32" + ></iconify-icon> <div class="my-auto">{$t("user.showBalance")}</div> </button> {:else if !isNaN(rate)} - <Amount amount={balance} {rate} {currency} {locale} align="left" /> + <div class="flex gap-4"> + <Amount + amount={Math.max(balance - (id === user.id ? (locked || 0) : 0), 0)} + {rate} + {currency} + {locale} + align="left" + /> + {#if locked && id === user.id} + <div class="text-red-600"> + <Amount + amount={locked} + {rate} + {currency} + {locale} + align="left" + locked={true} + /> + </div> + {/if} + </div> {:else} <div class="text-gray-200">—</div> {/if} diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte index 0cab1d13..8d2275ca 100644 --- a/src/components/Footer.svelte +++ b/src/components/Footer.svelte @@ -5,7 +5,7 @@ const followLinks = [ { - url: "nostr:nprofile1qqst4qyeqenw7zm0fwjsty68h6cnys5jre2xd8ngqpjv5a2j26s78fspz4mhxue69uhhyetvv9ujucm0d9hx7uewd9hsgwx239", + url: "nostr:nprofile1qy28wue69uhnzv3h9cczuvpwxyargwpk8yhszxrhwvaz7tmhda6zucnjd9nksarzdak8gtnwv46z7qpqw27mc4aa6m0ufe3xs5z3m6qyr52gc0rglept7vqlwx4xeaf72tasv5xyqv", titleID: "nostr", }, { url: "https://t.me/coinoswallet", titleID: "telegram" }, diff --git a/src/components/InvoiceActions.svelte b/src/components/InvoiceActions.svelte index b11a66c3..e2a31ab8 100644 --- a/src/components/InvoiceActions.svelte +++ b/src/components/InvoiceActions.svelte @@ -45,62 +45,6 @@ <div class="my-auto text-lg">Liquid</div> </button> - <div class="flex gap-2"> - <button - class="btn flex-nowrap !w-auto grow" - class:bg-base-300={type === types.lightning} - class:text-secondary={type === types.lightning} - onclick={() => { - $amountPrompt = false; - newAmount = undefined; - setType(types.lightning); - }} - > - <div - class="bg-black rounded-full w-8 h-8 items-center justify-center flex" - > - <div class="m-auto"> - <iconify-icon - noobserver - icon="ph:lightning-fill" - class="text-yellow-300 text-2xl" - ></iconify-icon> - </div> - </div> - <div class="my-auto text-lg">Lightning</div> - </button> - - <button - class="btn flex-nowrap !w-auto grow" - class:bg-base-300={type === types.bitcoin} - class:text-secondary={type === types.bitcoin} - onclick={() => setType(types.bitcoin)} - > - <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" /> - <div class="my-auto text-lg">Bitcoin</div> - </button> - </div> - - <div class="flex flex-wrap gap-2 justify-around text-secondary w-full"> - <button type="button" class="btn !w-auto grow" onclick={() => copy(txt)}> - <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon> - <div class="my-auto"> - {t("payments.copy")} - </div> - </button> - - <button class="btn grow !w-auto" onclick={() => goto("/receive")}> - <iconify-icon noobserver icon="ph:clipboard-text-bold" width="32" - ></iconify-icon> - <div class="my-auto text-lg">{t("user.send.paste")}</div> - </button> - - <button class="btn grow !w-auto" onclick={() => goto("/scan")}> - <iconify-icon noobserver icon="ph:camera-bold" width="32"></iconify-icon> - <div class="my-auto text-lg">{t("user.send.scan")}</div> - </button> - </div> - <div class="flex flex-wrap gap-2 justify-around text-secondary w-full"> {#if user?.id === invoice?.user?.id} <button class="btn grow !w-auto" onclick={toggleAmount}> @@ -127,6 +71,26 @@ </button> </div> + <div class="flex flex-wrap gap-2 justify-around text-secondary w-full"> + <button type="button" class="btn !w-auto grow" onclick={() => copy(txt)}> + <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon> + <div class="my-auto"> + {t("payments.copy")} + </div> + </button> + + <button class="btn grow !w-auto" onclick={() => goto("/receive")}> + <iconify-icon noobserver icon="ph:clipboard-text-bold" width="32" + ></iconify-icon> + <div class="my-auto text-lg">{t("user.send.paste")}</div> + </button> + + <button class="btn grow !w-auto" onclick={() => goto("/scan")}> + <iconify-icon noobserver icon="ph:camera-bold" width="32"></iconify-icon> + <div class="my-auto text-lg">{t("user.send.scan")}</div> + </button> + </div> + {#if txt.length > 120} <div class="w-full flex justify-center gap-2 flex-wrap"> <button class="btn" onclick={() => (showQr = !showQr)}> @@ -144,6 +108,43 @@ </div> {/if} + <div class="flex gap-2"> + <button + class="btn flex-nowrap !w-auto grow" + class:bg-base-300={type === types.lightning} + class:text-secondary={type === types.lightning} + onclick={() => { + $amountPrompt = false; + newAmount = undefined; + setType(types.lightning); + }} + > + <div + class="bg-black rounded-full w-8 h-8 items-center justify-center flex" + > + <div class="m-auto"> + <iconify-icon + noobserver + icon="ph:lightning-fill" + class="text-yellow-300 text-2xl" + ></iconify-icon> + </div> + </div> + <div class="my-auto text-lg">Lightning</div> + </button> + + <button + class="btn flex-nowrap !w-auto grow" + class:bg-base-300={type === types.bitcoin} + class:text-secondary={type === types.bitcoin} + onclick={() => setType(types.bitcoin)} + > + <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" /> + <div class="my-auto text-lg">Bitcoin</div> + </button> + </div> + + <button type="button" class="btn" onclick={toggleType}> <img src="/images/liquid.svg" class="w-8" alt="Liquid" /> <img src="/images/cash.png" class="w-8 my-auto" /> diff --git a/src/components/Menu.svelte b/src/components/Menu.svelte index 7408687c..e433fefb 100644 --- a/src/components/Menu.svelte +++ b/src/components/Menu.svelte @@ -1,7 +1,7 @@ <script> import { theme } from "$lib/store"; import DarkToggle from "$comp/DarkToggle.svelte"; - import OutClick from "svelte-outclick"; + import { OutClick } from "svelte-outclick"; let { t, user, w, opacity } = $props(); let dark = () => ($theme = $theme === "dark" ? "light" : "dark"); @@ -11,11 +11,11 @@ { key: "nav.settings", icon: "ph:gear-bold", href: `/settings` }, { key: "nav.support", icon: "ph:lifebuoy-bold", href: `/support` }, { key: "nav.map", icon: "ph:map-trifold-bold", href: `/map` }, - { - key: "nav.merch", - icon: "ph:storefront-bold", - href: `https://coinosmerch.com`, - }, + // { + // key: "nav.merch", + // icon: "ph:storefront-bold", + // href: `https://coinosmerch.com`, + // }, { key: "nav.dark", icon: "ph:moon-stars-bold", href: `/dark` }, { key: "nav.signOut", icon: "ph:sign-out-bold", href: `/logout` }, ]; @@ -25,7 +25,7 @@ </script> <div> - <OutClick on:outclick={hideMenu}> + <OutClick onOutClick={hideMenu}> <button class="btn-menu {opacity('/support')}" onclick={toggleMenu} ><iconify-icon noobserver icon="ph:list-bold" width={w > 640 ? 32 : 24} ></iconify-icon> diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte index ad70cd31..a616194b 100644 --- a/src/components/Nostr.svelte +++ b/src/components/Nostr.svelte @@ -13,7 +13,6 @@ getPublicKey, } from "nostr-tools"; import { getConversationKey, decrypt } from "nostr-tools/nip44"; - import { AmberClipboardSigner } from "applesauce-signer"; import { copy, focus, fail, post } from "$lib/utils"; import { eventToSign, signer } from "$lib/store"; @@ -105,14 +104,6 @@ $signer = { method: "extension", ready: true }; }; - let signerGetPubkey = async () => { - const pubkey = await new AmberClipboardSigner().getPublicKey(); - $signer = { - method: "signer", - params: { pubkey }, - }; - }; - let signerSign = async () => { document.addEventListener("focus", signerSigned, true); }; diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte index d80f0400..c321e0bc 100644 --- a/src/components/Numpad.svelte +++ b/src/components/Numpad.svelte @@ -1,23 +1,30 @@ <script> import { onMount } from "svelte"; - import { fiat } from "$lib/store"; import Left from "$comp/Left.svelte"; - import Icon from "$comp/Icon.svelte"; import { f, s, focus, warning, sat, sats } from "$lib/utils"; import { t } from "$lib/translations"; + import { fiat as fiatStore } from "$lib/store"; - export let amount = 0, + // ---- Props (Svelte 5 runes) ---- + let { + amount = $bindable(), currency = "USD", + fiat = $bindable(), element, - rate, + rate = $bindable(), locale, - submit = undefined; + submit = undefined, + amountFiat = 0, + } = $props(); - export let amountFiat = 0; + // ---- Local state ---- + let fiatDigits = $state("0"); // cents buffer in fiat mode + let html = $state("0"); - let pasted = async () => { - html = (await navigator.clipboard.readText()).replace(/,/g, ""); - }; + // currency info as reactive state + let decimalChar = $state("."); + let symbol = $state(""); + let position = $state("before"); function getCurrencyInfo(locale = "en-US", currency) { const formatter = new Intl.NumberFormat(locale, { @@ -27,168 +34,232 @@ minimumFractionDigits: 2, maximumFractionDigits: 2, }); - const parts = formatter.formatToParts(123456.78); - const { value: decimal } = parts.find((part) => part.type === "decimal"); - const currencyPart = parts.find((part) => part.type === "currency"); - const symbol = currencyPart ? currencyPart.value : ""; + const { value: decimal } = parts.find((p) => p.type === "decimal"); + const currencyPart = parts.find((p) => p.type === "currency"); + const sym = currencyPart ? currencyPart.value : ""; + const firstNumberIndex = parts.findIndex((p) => p.type === "integer"); + const currencyIndex = parts.findIndex((p) => p.type === "currency"); + const pos = currencyIndex < firstNumberIndex ? "before" : "after"; + return { symbol: sym, position: pos, decimal }; + } - const firstNumberIndex = parts.findIndex((part) => part.type === "integer"); - const currencyIndex = parts.findIndex((part) => part.type === "currency"); - const position = currencyIndex < firstNumberIndex ? "before" : "after"; + // reflect external prop/locale changes + $effect(() => { + const info = getCurrencyInfo(locale, currency); + decimalChar = info.decimal; + symbol = info.symbol; + position = info.position; + }); - return { symbol, position, decimal }; + function formatFiatFromDigits(digits, dec = ".") { + let d = digits.replace(/\D+/g, ""); + if (!d) d = "0"; + if (d.length > 15) d = d.slice(0, 15); + if (d.length === 1) d = "0" + d; + if (d.length === 2) d = "0" + d; + const whole = d.slice(0, -2).replace(/^0+(?=\d)/, "") || "0"; + const cents = d.slice(-2); + return `${whole}${dec}${cents}`; } - function strip(input) { - const regex = new RegExp(`[^0-9\.]`, "g"); - return input.replace(decimal, ".").replace(regex, ""); + function normalizeSats(str) { + const digits = (str ?? "").replace(/[^\d]/g, ""); + const noLead = digits.replace(/^0+(?=\d)/, ""); + return noLead || "0"; } - let { decimal, symbol, position } = getCurrencyInfo(locale, currency); - - let arrow = "<"; - - $: update(amount, amountFiat); - let update = (a, f) => { - if ($fiat) { - amount = f ? Math.round(strip(f) / (rate / sats)) : 0; + const updateByMode = () => { + if (fiat) { + const numericFiat = + parseFloat((fiatDigits || "0").replace(/\D/g, "")) / 100; + amountFiat = isFinite(numericFiat) ? numericFiat.toFixed(2) : "0.00"; + amount = numericFiat ? Math.round((numericFiat * sats) / rate) : 0; } else { - amountFiat = a ? ((a * rate) / sats).toFixed(2) : 0; + // integer-only sats, already normalized + const a = parseInt((html || "0").replace(/[^\d]/g, "")) || 0; + amount = a || 0; + const fval = amount ? (amount * rate) / sats : 0; + amountFiat = fval ? fval.toFixed(2) : "0.00"; } }; - let loading = false; - - const numPad = [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - decimal, - "0", - "<", - ]; - - const handleInput = (value) => { - if (value === "<") html = html.substr(0, html.length - 1); - else html += value; + const pasted = async () => { + const txt = (await navigator.clipboard.readText()) ?? ""; + if (fiat) { + const m = txt.replace(/[^\d.,]/g, ""); + const normalized = m.replace(new RegExp(`[${decimalChar}]`, "g"), "."); + const val = parseFloat(normalized); + const cents = isFinite(val) ? Math.round(val * 100) : 0; + fiatDigits = Math.max(0, cents).toString(); + html = formatFiatFromDigits(fiatDigits, decimalChar); + } else { + const clean = normalizeSats(txt); + html = clean; + element && (element.innerHTML = html); + } input(null, true); }; - $: html = $fiat ? amountFiat : (amount || 0).toString(); - - let prev = ""; - - let input = (e, numpad = false) => { - let d = decimal.replace(".", "\\."); - - if (!$fiat && html.includes(decimal)) { - html = html.replace(new RegExp(d), ""); - } - - if (prev === "0" && html !== `0${decimal}`) { - prev = ""; - html = html.replace("0", ""); - } - - let sel = getSelection(); - let i = sel.focusOffset; - - if (html[0] === "0") i = html.length; - - if (html.split(decimal)[0][0] === "0" && html.length > 1) { - html = html.replace("0", ""); - i++; - } + let prevHtml = $state(""); - if (html[0] === decimal) { - html = `0${html}`; - i = html.length; - } + // (kept for completeness; no longer used in sats path) + function trimTrailingZeros(str) { + if (!str.includes(".")) return str; + let out = str.replace(/(\.\d*?[1-9])0+$/, "$1"); + out = out.replace(/\.0*$/, ""); + return out; + } - if (html.includes(decimal) && html.split(decimal)[1].length > 2) { - html = html.slice(0, html.indexOf(decimal) + 3); - i = html.length; + const input = (e, fromPad = false) => { + if (fiat) { + if (!fromPad) { + const raw = (element?.textContent ?? "").replace(/[^\d]/g, ""); + fiatDigits = raw || "0"; + } + const cents = parseInt(fiatDigits || "0") || 0; + const numericFiat = cents / 100; + const satsEstimate = Math.round((numericFiat * sats) / rate); + if (satsEstimate > sats && html.length > prevHtml.length) { + fiatDigits = fiatDigits.slice(0, -1) || "0"; + warning($t("user.receive.lessThan1BTCWarning")); + } + html = formatFiatFromDigits(fiatDigits, decimalChar); + } else { + // integer-only sats, normalize leading zeros + let clean = normalizeSats(element?.textContent ?? ""); + const maybe = parseInt(clean) || 0; + if (maybe > sats && clean.length > prevHtml.length) { + clean = normalizeSats(clean.slice(0, -1)); + warning($t("user.receive.lessThan1BTCWarning")); + } + html = clean; + element && (element.innerHTML = html); } - let clean = html - .substr(0, 15) - .replace(new RegExp(`[^0-9${d}]+`, "g"), "") - .replace(new RegExp(d), "X") - .replace(new RegExp(d, "g"), "") - .replace("X", decimal); + updateByMode(); - if (clean !== html) { - html = clean; - i = html.length; - } - if (amount > sats && html.length > prev.length) { - html = html.substr(0, html.length - 1); - i = html.length; - warning($t("user.receive.lessThan1BTCWarning")); + if (!fromPad) { + queueMicrotask(() => { + const node = element?.childNodes?.[0]; + if (!node) return; + const range = document.createRange(); + const sel = getSelection(); + const L = node.length ?? node.textContent?.length ?? 0; + range.setStart(node, L); + range.setEnd(node, L); + sel.removeAllRanges(); + sel.addRange(range); + }); } + prevHtml = html.toString(); + }; - if ($fiat) { - amountFiat = html; + const handleInput = (value) => { + if (fiat) { + if (value === arrow) { + fiatDigits = fiatDigits.length > 1 ? fiatDigits.slice(0, -1) : "0"; + } else if (value === "00") { + fiatDigits = (fiatDigits === "0" ? "" : fiatDigits) + "00"; + if (!fiatDigits) fiatDigits = "0"; + } else { + fiatDigits = (fiatDigits === "0" ? "" : fiatDigits) + value; + } + html = formatFiatFromDigits(fiatDigits, decimalChar); } else { - amount = parseInt(html); - } - - if (!amount) amount = null; - - if (!numpad) { - setTimeout(() => { - let node = element.childNodes[0]; - let range = document.createRange(); - - if (node) { - if (i > node.length) i = node.length; - range.setStart(node, i); - range.setEnd(node, i); - - let sel = getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - node.selectionStart = node.selectionEnd; - } - - prev = html.toString(); - }, 0); + if (value === arrow) { + html = html.length > 1 ? html.slice(0, -1) : "0"; + } else if (value === "00") { + // If current is "0", keep it "0"; otherwise append two zeros + html = html === "0" ? "0" : html + "00"; + } else { + // If current is "0", replace with the digit; else append + html = html === "0" ? value : html + value; + } + // normalize away any accidental leading zeros + html = normalizeSats(html); + element && (element.innerHTML = html); } + input(null, true); }; - let select = (e) => { - let range = document.createRange(); + const select = (e) => { + const range = document.createRange(); range.selectNodeContents(e.target); - let sel = getSelection(); + const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range); }; - let blur = () => { - if (!html) html = "0"; + const blur = () => { + if (!html) html = fiat ? formatFiatFromDigits("0", decimalChar) : "0"; }; - let keydown = (e) => - e.key === "Enter" && (e.preventDefault() || submit.click()); + const keydown = (e) => + e.key === "Enter" && (e.preventDefault() || submit?.click?.()); - let swap = () => { - if ($fiat) { - amount = parseInt((amountFiat / (rate / sats)).toFixed(0)); + // CHANGED: make swap set html for the new mode immediately and keep DOM in sync + const swap = () => { + if (fiat) { + // going -> sats + fiat = false; + html = normalizeSats((amount || 0).toString()); + element && (element.innerHTML = html); } else { - amountFiat = - (amount * (rate / sats)).toFixed(2) > 0.0 - ? (amount * (rate / sats)).toFixed(2) - : 0; + // going -> fiat + const cents = Math.round((parseFloat(amountFiat || "0") || 0) * 100); + fiatDigits = Math.max(0, cents).toString(); + fiat = true; + html = formatFiatFromDigits(fiatDigits, decimalChar); + element && (element.innerHTML = html); } - $fiat = !$fiat; + updateByMode(); + prevHtml = html.toString(); + + // Move caret to end after swap + queueMicrotask(() => { + const node = element?.childNodes?.[0]; + if (!node) return; + const range = document.createRange(); + const sel = getSelection(); + const L = node.length ?? node.textContent?.length ?? 0; + range.setStart(node, L); + range.setEnd(node, L); + sel.removeAllRanges(); + sel.addRange(range); + }); + + $fiatStore = fiat; }; + + const arrow = "<"; + const numPad = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "00", + "0", + arrow, + ]; + + onMount(() => { + fiat = $fiatStore; + if (fiat) { + const cents = Math.round((parseFloat(amountFiat || "0") || 0) * 100); + fiatDigits = Math.max(0, cents).toString(); + html = formatFiatFromDigits(fiatDigits, decimalChar); // keep html in sync on mount + } else { + html = normalizeSats((amount || 0).toString()); + } + element && (element.innerHTML = html); + updateByMode(); + }); </script> <div class="flex justify-center items-center"> @@ -197,11 +268,9 @@ <div class="text-5xl md:text-6xl font-semibold tracking-widest flex justify-center" > - <div class="my-auto" class:text-5xl={!$fiat}> - {#if $fiat} - {#if position === "before"} - {symbol} - {/if} + <div class="my-auto" class:text-5xl={!fiat}> + {#if fiat} + {#if position === "before"}{symbol}{/if} {:else} <iconify-icon noobserver @@ -210,27 +279,47 @@ ></iconify-icon> {/if} </div> + + <!-- contenteditable input --> <div use:focus + role="textbox" + tabindex="0" + aria-label="Amount input" + aria-multiline="false" contenteditable bind:innerHTML={html} - on:paste={pasted} - on:focus={select} - on:blur={blur} - on:input={input} - on:keydown={keydown} + inputmode={fiat ? "decimal" : "numeric"} + enterkeyhint="done" + autocapitalize="off" + autocorrect="off" + spellcheck="false" + onpaste={pasted} + onfocus={(e) => { + select(e); + }} + onblur={blur} + oninput={input} + onkeydown={keydown} class="outline-none my-auto" bind:this={element} - /> - {#if $fiat && position === "after"} - {symbol} - {/if} + ></div> + + {#if fiat && position === "after"}{symbol}{/if} </div> - <div - class="flex items-center justify-center text-2xl cursor-pointer" - on:click|preventDefault|stopPropagation={swap} + + <!-- swap button --> + <button + type="button" + class="flex items-center justify-center text-2xl cursor-pointer mx-auto select-none" + aria-label="Swap currency display" + onclick={(e) => { + e.preventDefault(); + e.stopPropagation(); + swap(); + }} > - {#if $fiat} + {#if fiat} <iconify-icon noobserver icon="ph:lightning-fill" @@ -240,7 +329,7 @@ {:else} {f(amountFiat, currency, locale)} {/if} - </div> + </button> </div> <div class="grid grid-cols-3 gap-2 w-full mx-auto"> @@ -249,7 +338,12 @@ <button type="button" class="btn" - on:click|preventDefault|stopPropagation={() => handleInput(value)} + aria-label="Backspace" + onclick={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleInput(value); + }} > <Left /> </button> @@ -257,9 +351,15 @@ <button type="button" class="btn" - on:click|preventDefault|stopPropagation={() => handleInput(value)} - >{value}</button + aria-label={`Key ${value}`} + onclick={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleInput(value); + }} > + {value} + </button> {/if} {/each} </div> diff --git a/src/components/Payments.svelte b/src/components/Payments.svelte index 814a5b4e..36633089 100644 --- a/src/components/Payments.svelte +++ b/src/components/Payments.svelte @@ -6,6 +6,7 @@ import { t } from "$lib/translations"; import locales from "$lib/locales"; const { fund, locale, user, payments } = $props(); + const language = $derived(user?.language || "en"); </script> <div class="text-base"> @@ -106,7 +107,11 @@ {#if p.type === types.ecash} {amount > 0 ? $t("payments.redeemed") : $t("payments.minted")} {:else} - {amount > 0 ? (p.confirmed ? $t("payments.received") : $t("payments.pending")) : $t("payments.sent")} + {amount > 0 + ? p.confirmed + ? $t("payments.received") + : $t("payments.pending") + : $t("payments.sent")} {/if} </div> </div> @@ -116,12 +121,12 @@ <div class="text-secondary text-right text-sm my-auto col-span-3"> <div> {format(new Date(p.created), "h:mm aaa", { - locale: locales[user.language], + locale: locales[language], })} </div> <div> {format(new Date(p.created), "MMM d, yy", { - locale: locales[user.language], + locale: locales[language], })} </div> </div> diff --git a/src/components/Pin.svelte b/src/components/Pin.svelte index a429e976..980e17fa 100644 --- a/src/components/Pin.svelte +++ b/src/components/Pin.svelte @@ -20,7 +20,7 @@ let loaded; onMount(() => setTimeout(() => (loaded = true), 500)); - let locktime = 30 * 60; + let locktime = 5 * 60; $: loaded && checkPin(p); let checkPin = async (p) => { @@ -34,7 +34,10 @@ if (result) { if (notify) success("Pin confirmed"); - if (locktime) setCookie("pin", p, locktime); + if (locktime) { + setCookie("pin", p, locktime); + setTimeout(() => ($pin = undefined), locktime * 1000); + } $pin = p; } else { fail("Invalid pin"); @@ -63,7 +66,6 @@ <select name="locktime" bind:value={locktime}> <option value={30}>30 {$t("user.settings.seconds")}</option> <option value={5 * 60}>5 {$t("user.settings.minutes")}</option> - <option value={10 * 60}>10 {$t("user.settings.minutes")}</option> <option value={30 * 60}>30 {$t("user.settings.minutes")}</option> <option value={60 * 60}>1 {$t("user.settings.hour")}</option> <option value={8 * 60 * 60}>8 {$t("user.settings.hours")}</option> diff --git a/src/components/Qr.svelte b/src/components/Qr.svelte index 72655ebc..da4ceb00 100644 --- a/src/components/Qr.svelte +++ b/src/components/Qr.svelte @@ -1,18 +1,18 @@ <script> import screenfull from "screenfull"; - let { text } = $props(); + let { icon, text } = $props(); let qr = $state(); + + let src = `/qr/${encodeURIComponent(text)}`; + if (icon) src += `/${icon}`; + else src += "/raw"; </script> <div - class="w-80 mx-auto cursor-pointer" + class="w-72 sm:w-80 mx-auto cursor-pointer" bind:this={qr} onclick={() => screenfull.toggle(qr)} onkeydown={() => screenfull.toggle(qr)} > - <img - src={`/qr/${encodeURIComponent(text)}/raw`} - class="mx-auto" - alt="QR Code" - /> + <img {src} class="mx-auto" alt="QR Code" /> </div> diff --git a/src/components/SetAmount.svelte b/src/components/SetAmount.svelte index a453b9d1..536c8e24 100644 --- a/src/components/SetAmount.svelte +++ b/src/components/SetAmount.svelte @@ -6,13 +6,13 @@ let submit = $state(); let { - currency = $bindable(), - fiat = $bindable(), - locale = $bindable(), - rate = $bindable(), + currency = $bindable("USD"), + fiat = $bindable(true), + locale = $bindable("en-US"), + rate = $bindable(0), settingAmount, setAmount, - newAmount = $bindable(), + newAmount = $bindable(0), toggleAmount, t, } = $props(); |
