mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 06:44:27 +01:00
use dialog in amounteditable
This commit is contained in:
@@ -13,8 +13,7 @@ export function Amount(props: { amountSats: bigint | number | undefined, showFia
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
async function getPrice() {
|
||||
// return await state.node_manager?.get_bitcoin_price()
|
||||
return 30000
|
||||
return await state.node_manager?.get_bitcoin_price()
|
||||
}
|
||||
|
||||
const [price] = createResource(getPrice)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { For, Show, createEffect, createMemo, createResource, createSignal } from 'solid-js';
|
||||
import { For, createMemo, createResource, createSignal } from 'solid-js';
|
||||
import { Button } from '~/components/layout';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { satsToUsd } from '~/utils/conversions';
|
||||
import { Amount } from './Amount';
|
||||
import { Dialog } from '@kobalte/core';
|
||||
|
||||
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "⌫"];
|
||||
const FULLSCREEN_STYLE = 'fixed top-0 left-0 w-screen h-screen z-50 bg-sidebar-gray p-4';
|
||||
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "DEL"];
|
||||
|
||||
function SingleDigitButton(props: { character: string, onClick: (c: string) => void }) {
|
||||
return (
|
||||
@@ -18,20 +18,13 @@ function SingleDigitButton(props: { character: string, onClick: (c: string) => v
|
||||
);
|
||||
}
|
||||
|
||||
export function AmountEditable(props: { amountSats: string, setAmountSats: (s: string) => void, onSave?: () => void }) {
|
||||
const [isFullscreen, setIsFullscreen] = createSignal(false);
|
||||
|
||||
function toggleFullscreen() {
|
||||
setIsFullscreen(!isFullscreen());
|
||||
}
|
||||
|
||||
// TODO: validate this doesn't need to be reactive and can be "initialAmountSats"
|
||||
const [displayAmount, setDisplayAmount] = createSignal(props.amountSats || "0");
|
||||
export function AmountEditable(props: { initialAmountSats: string, setAmountSats: (s: bigint) => void, onSave?: () => void }) {
|
||||
const [displayAmount, setDisplayAmount] = createSignal(props.initialAmountSats || "0");
|
||||
|
||||
let inputRef!: HTMLInputElement;
|
||||
|
||||
function handleCharacterInput(character: string) {
|
||||
if (character === "⌫") {
|
||||
if (character === "DEL") {
|
||||
setDisplayAmount(displayAmount().slice(0, -1));
|
||||
} else {
|
||||
if (displayAmount() === "0") {
|
||||
@@ -45,13 +38,6 @@ export function AmountEditable(props: { amountSats: string, setAmountSats: (s: s
|
||||
inputRef.focus()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (isFullscreen()) {
|
||||
inputRef.focus();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// making a "controlled" input is a known hard problem
|
||||
// https://github.com/solidjs/solid/discussions/416
|
||||
function handleHiddenInput(e: Event & {
|
||||
@@ -113,8 +99,7 @@ export function AmountEditable(props: { amountSats: string, setAmountSats: (s: s
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
async function getPrice() {
|
||||
// return await state.node_manager?.get_bitcoin_price()
|
||||
return 30000
|
||||
return await state.node_manager?.get_bitcoin_price()
|
||||
}
|
||||
|
||||
const [price] = createResource(getPrice)
|
||||
@@ -123,14 +108,15 @@ export function AmountEditable(props: { amountSats: string, setAmountSats: (s: s
|
||||
// What we're all here for in the first place: returning a value
|
||||
function handleSubmit() {
|
||||
// validate it's a number
|
||||
console.log("handling submit...");
|
||||
const number = Number(displayAmount());
|
||||
if (isNaN(number) || number < 0) {
|
||||
setDisplayAmount("0");
|
||||
inputRef.focus();
|
||||
return;
|
||||
} else {
|
||||
props.setAmountSats(displayAmount());
|
||||
toggleFullscreen();
|
||||
const bign = BigInt(displayAmount());
|
||||
props.setAmountSats(bign);
|
||||
// This is so the parent can focus the next input if it wants to
|
||||
if (props.onSave) {
|
||||
props.onSave();
|
||||
@@ -138,22 +124,31 @@ export function AmountEditable(props: { amountSats: string, setAmountSats: (s: s
|
||||
}
|
||||
}
|
||||
|
||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom"
|
||||
const DIALOG_CONTENT = "h-screen-safe p-4 bg-gray/50 backdrop-blur-md bg-black/80"
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: a better transition than this */}
|
||||
<div class={`cursor-pointer transition-all ${isFullscreen() && FULLSCREEN_STYLE}`}>
|
||||
<Show when={isFullscreen()} fallback={<div class="p-4 rounded-xl border-2 border-m-blue" onClick={toggleFullscreen}>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<div class="p-4 rounded-xl border-2 border-m-blue">
|
||||
<Amount amountSats={Number(displayAmount())} showFiat />
|
||||
</div>}>
|
||||
<input ref={el => inputRef = el}
|
||||
type="text"
|
||||
class="opacity-0 absolute -z-10"
|
||||
value={displayAmount()}
|
||||
onInput={(e) => handleHiddenInput(e)}
|
||||
/>
|
||||
<div class="w-full h-full max-w-[600px] mx-auto">
|
||||
<div class="flex flex-col gap-4 h-full">
|
||||
<div class="p-4 bg-black rounded-xl flex-1 flex flex-col gap-4 items-center justify-center">
|
||||
</div>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
{/* TODO: figure out how to submit on enter */}
|
||||
<input ref={el => inputRef = el}
|
||||
autofocus
|
||||
inputmode='none'
|
||||
type="text"
|
||||
class="opacity-0 absolute -z-10"
|
||||
value={displayAmount()}
|
||||
onInput={(e) => handleHiddenInput(e)}
|
||||
/>
|
||||
<div class="flex flex-col gap-4 max-w-[400px] mx-auto">
|
||||
<div class="p-4 bg-black rounded-xl flex flex-col gap-4 items-center justify-center">
|
||||
<h1 class={`font-light text-center transition-transform ease-out duration-300 text-6xl ${scale()}`}>
|
||||
{prettyPrint()} <span class='text-xl'>SATS</span>
|
||||
</h1>
|
||||
@@ -168,18 +163,18 @@ export function AmountEditable(props: { amountSats: string, setAmountSats: (s: s
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
{/* TODO: this feels wrong */}
|
||||
<Dialog.CloseButton>
|
||||
<Button intent="inactive" class="w-full flex-none"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Set Amount
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { TextField } from "@kobalte/core";
|
||||
import { Match, Suspense, Switch, createEffect, createMemo, createResource, createSignal } from "solid-js";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToUsd, usdToSats } from "~/utils/conversions";
|
||||
|
||||
export type AmountInputProps = {
|
||||
initialAmountSats: string;
|
||||
setAmountSats: (amount: string) => void;
|
||||
refSetter: (el: HTMLInputElement) => void;
|
||||
}
|
||||
|
||||
type ActiveCurrency = "usd" | "sats"
|
||||
|
||||
export function AmountInput(props: AmountInputProps) {
|
||||
// We need to keep a local amount state because we need to convert between sats and USD
|
||||
// But we should keep the parent state in sats
|
||||
const [localAmount, setLocalAmount] = createSignal(props.initialAmountSats || "0");
|
||||
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
async function getPrice() {
|
||||
// return await state.node_manager?.get_bitcoin_price()
|
||||
return 30000
|
||||
}
|
||||
|
||||
const [activeCurrency, setActiveCurrency] = createSignal<ActiveCurrency>("sats")
|
||||
|
||||
const [price] = createResource(getPrice)
|
||||
|
||||
const amountInUsd = createMemo(() => satsToUsd(price(), parseInt(localAmount()) || 0, true))
|
||||
const amountInSats = createMemo(() => usdToSats(price(), parseFloat(localAmount() || "0.00") || 0, true))
|
||||
|
||||
createEffect(() => {
|
||||
// When the local amount changes, update the parent state if we're in sats
|
||||
if (activeCurrency() === "sats") {
|
||||
props.setAmountSats(localAmount())
|
||||
} else {
|
||||
// If we're in USD, convert the amount to sats
|
||||
props.setAmountSats(usdToSats(price(), parseFloat(localAmount() || "0.00") || 0, false))
|
||||
}
|
||||
})
|
||||
|
||||
function toggleActiveCurrency() {
|
||||
if (activeCurrency() === "sats") {
|
||||
setActiveCurrency("usd")
|
||||
// Convert the current amount of sats to USD
|
||||
const usd = satsToUsd(price() || 0, parseInt(localAmount()) || 0, false)
|
||||
console.log(`converted ${localAmount()} sats to ${usd} USD`)
|
||||
setLocalAmount(usd);
|
||||
} else {
|
||||
setActiveCurrency("sats")
|
||||
// Convert the current amount of USD to sats
|
||||
const sats = usdToSats(price() || 0, parseInt(localAmount()) || 0, false)
|
||||
console.log(`converted ${localAmount()} usd to ${sats} sats`)
|
||||
setLocalAmount(sats)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="">
|
||||
<TextField.Root
|
||||
value={localAmount()}
|
||||
onValueChange={setLocalAmount}
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<pre>{`Bitcoin is ${price()?.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })}`}</pre>
|
||||
<TextField.Label class="text-sm font-semibold uppercase" >Amount {activeCurrency() === "sats" ? "(sats)" : "(USD)"}</TextField.Label>
|
||||
<TextField.Input autofocus ref={(el) => props.refSetter(el)} inputmode={"decimal"} class="w-full p-2 rounded-lg text-black" />
|
||||
<Suspense>
|
||||
<Switch fallback={<div>Loading...</div>}>
|
||||
<Match when={price() && activeCurrency() === "sats"}>
|
||||
<pre>{`~${amountInUsd()}`}</pre>
|
||||
</Match>
|
||||
<Match when={price() && activeCurrency() === "usd"}>
|
||||
<pre>{`${amountInSats()} sats`}</pre>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</TextField.Root>
|
||||
<button type="button" onClick={toggleActiveCurrency}>🔀</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,7 @@ import logo from '~/assets/icons/mutiny-logo.svg';
|
||||
import { DefaultMain, NodeManagerGuard, SafeArea } from "~/components/layout";
|
||||
import BalanceBox from "~/components/BalanceBox";
|
||||
import NavBar from "~/components/NavBar";
|
||||
|
||||
// TODO: use this reload prompt for real
|
||||
import ReloadPrompt from "~/components/Reload";
|
||||
import KitchenSink from './KitchenSink';
|
||||
import { Scan } from '~/assets/svg/Scan';
|
||||
import { A } from 'solid-start';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createResource, createSignal, Show, Suspense } from "solid-js";
|
||||
import { Button, ButtonLink, FancyCard } from "~/components/layout";
|
||||
import { createResource, Show, Suspense } from "solid-js";
|
||||
import { ButtonLink, FancyCard } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Amount } from "./Amount";
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ export function StyledRadioGroup(props: { value: string, choices: Choices, onVal
|
||||
{choice =>
|
||||
<RadioGroup.Item value={choice.value} class="ui-checked:bg-white bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2">
|
||||
<div class="py-3 px-4">
|
||||
<RadioGroup.ItemInput class="radio__input " />
|
||||
<RadioGroup.ItemControl class="radio__control">
|
||||
<RadioGroup.ItemIndicator class="radio__indicator" />
|
||||
<RadioGroup.ItemInput />
|
||||
<RadioGroup.ItemControl >
|
||||
<RadioGroup.ItemIndicator />
|
||||
</RadioGroup.ItemControl>
|
||||
<RadioGroup.ItemLabel class="ui-checked:text-m-blue text-neutral-400">
|
||||
<div class="block">
|
||||
|
||||
@@ -38,8 +38,8 @@ const FancyCard: ParentComponent<{ title?: string, tag?: JSX.Element }> = (props
|
||||
|
||||
const SafeArea: ParentComponent = (props) => {
|
||||
return (
|
||||
<div class="safe-top safe-left safe-right safe-bottom">
|
||||
<div class="disable-scrollbars max-h-screen h-full overflow-y-scroll md:pl-[8rem] md:pr-[6rem]">
|
||||
<div class="safe-top safe-left safe-right safe-bottom flex flex-col h-screen-safe">
|
||||
<div class="flex-1 disable-scrollbars overflow-y-scroll md:pl-[8rem] md:pr-[6rem]">
|
||||
{props.children}
|
||||
<div class="h-32" />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TextField } from "@kobalte/core";
|
||||
import { createMemo, createResource, createSignal, Match, Switch } from "solid-js";
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
import { AmountEditable } from "~/components/AmountEditable";
|
||||
import { AmountInput } from "~/components/AmountInput";
|
||||
import { Button, Card, DefaultMain, LargeHeader, NodeManagerGuard, SafeArea, SmallHeader } from "~/components/layout";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
@@ -106,7 +105,7 @@ export default function Receive() {
|
||||
<LargeHeader>Receive Bitcoin</LargeHeader>
|
||||
<Switch>
|
||||
<Match when={!unified() || receiveState() === "edit"}>
|
||||
<AmountEditable amountSats={amount() || "0"} setAmountSats={setAmount} onSave={handleAmountSave} />
|
||||
<AmountEditable initialAmountSats={amount() || "0"} setAmountSats={setAmount} onSave={handleAmountSave} />
|
||||
<div>
|
||||
<Button intent="glowy" layout="xs">Tag the sender</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RadioGroup, TextField } from "@kobalte/core";
|
||||
import { For, Show, createMemo, createResource, createSignal, onMount } from "solid-js";
|
||||
import { TextField } from "@kobalte/core";
|
||||
import { Show, createMemo, createResource, createSignal, onMount } from "solid-js";
|
||||
import { Amount } from "~/components/Amount";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { Button, ButtonLink, DefaultMain, FancyCard, InnerCard, LargeHeader, SafeArea, SmallHeader } from "~/components/layout";
|
||||
import { Button, ButtonLink, DefaultMain, LargeHeader, SafeArea, SmallHeader } from "~/components/layout";
|
||||
import { Paste } from "~/assets/svg/Paste";
|
||||
import { Scan } from "~/assets/svg/Scan";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
@@ -16,13 +16,14 @@ type SendSource = "lightning" | "onchain";
|
||||
|
||||
const PAYMENT_METHODS = [{ value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
|
||||
|
||||
const TEST_DEST = "bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl"
|
||||
// const TEST_DEST = "bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl"
|
||||
// const TEST_DEST_ADDRESS = "tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe"
|
||||
|
||||
export default function Send() {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
// These can only be set by the user
|
||||
const [destination, setDestination] = createSignal(TEST_DEST);
|
||||
const [destination, setDestination] = createSignal("");
|
||||
const [privateLabel, setPrivateLabel] = createSignal("");
|
||||
|
||||
// These can be derived from the "destination" signal or set by the user
|
||||
@@ -97,7 +98,7 @@ export default function Send() {
|
||||
}
|
||||
|
||||
// IMPORTANT: pass the signal but don't "call" the signal (`destination`, not `destination()`)
|
||||
const [_decodedDestination] = createResource(destination, decode);
|
||||
const [decodedDestination] = createResource(destination, decode);
|
||||
|
||||
let labelInput!: HTMLInputElement;
|
||||
|
||||
@@ -132,13 +133,12 @@ export default function Send() {
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<LargeHeader>Send Bitcoin</LargeHeader>
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
<SmallHeader>Destination</SmallHeader>
|
||||
</dt>
|
||||
<dd>
|
||||
<Show when={destination()} fallback={<div class="flex flex-row gap-4">
|
||||
<Show when={decodedDestination()} fallback={<div class="flex flex-row gap-4">
|
||||
<Button onClick={handlePaste}>
|
||||
<div class="flex flex-col gap-2 items-center">
|
||||
<Paste />
|
||||
@@ -174,7 +174,7 @@ export default function Send() {
|
||||
<div class="my-8 flex gap-4 w-full items-center justify-around">
|
||||
{/* if the amount came with the invoice we can't allow setting it */}
|
||||
<Show when={!(invoice()?.amount_sats)} fallback={<Amount amountSats={amountSats() || 0} showFiat />}>
|
||||
<AmountEditable amountSats={amountSats().toString() || "0"} setAmountSats={setAmountSats} />
|
||||
<AmountEditable initialAmountSats={amountSats().toString() || "0"} setAmountSats={setAmountSats} />
|
||||
</Show>
|
||||
<div class="bg-white/10 px-4 py-2 rounded-xl">
|
||||
<div class="flex gap-2 items-center">
|
||||
@@ -186,14 +186,13 @@ export default function Send() {
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="font-semibold uppercase text-white">Total</h2>
|
||||
<h3 class="text-xl font-light text-white">
|
||||
{(amountSats() + fakeFee()).toLocaleString()} <span class='text-lg'>SATS</span>
|
||||
{(amountSats().valueOf() + fakeFee().valueOf()).toLocaleString()} <span class='text-lg'>SATS</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</dd>
|
||||
|
||||
<Show when={address() && invoice()}>
|
||||
<dt>
|
||||
<SmallHeader>
|
||||
|
||||
@@ -13,9 +13,7 @@ export default function NotFound() {
|
||||
<p>
|
||||
This is probably Paul's fault.
|
||||
</p>
|
||||
<div class="h-full">
|
||||
|
||||
</div>
|
||||
<div class="h-full" />
|
||||
<ButtonLink href="/" intent="red">Dangit</ButtonLink>
|
||||
</DefaultMain>
|
||||
</SafeArea>
|
||||
|
||||
@@ -64,6 +64,12 @@ module.exports = {
|
||||
paddingBottom: 'constant(safe-area-inset-bottom)',
|
||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
||||
},
|
||||
'.h-screen-safe': {
|
||||
height: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||
},
|
||||
'.min-h-screen-safe': {
|
||||
minHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||
},
|
||||
'.disable-scrollbars': {
|
||||
scrollbarWidth: 'none',
|
||||
'-ms-overflow-style': 'none',
|
||||
|
||||
Reference in New Issue
Block a user