mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-20 06:34:24 +01:00
redesigned send and receive
This commit is contained in:
87
src/components/AmountCard.tsx
Normal file
87
src/components/AmountCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Match, ParentComponent, Show, Switch, createMemo } from "solid-js";
|
||||
import { Card, VStack } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { satsToUsd } from "~/utils/conversions";
|
||||
import { AmountEditable } from "./AmountEditable";
|
||||
|
||||
const KeyValue: ParentComponent<{ key: string, gray?: boolean }> = (props) => {
|
||||
return (
|
||||
<div class="flex justify-between items-center" classList={{ "text-neutral-400": props.gray }}>
|
||||
<div class="font-semibold uppercase">{props.key}</div>
|
||||
<div class="font-light">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const InlineAmount: ParentComponent<{ amount: string, sign?: string, fiat?: boolean }> = (props) => {
|
||||
const prettyPrint = createMemo(() => {
|
||||
const parsed = Number(props.amount);
|
||||
if (isNaN(parsed)) {
|
||||
return props.amount;
|
||||
} else {
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
})
|
||||
|
||||
return (<div class="inline-block text-lg">{props.sign ? `${props.sign} ` : ""}{props.fiat ? "$" : ""}{prettyPrint()} <span class="text-sm">{props.fiat ? "USD" : "SATS"}</span></div>)
|
||||
}
|
||||
|
||||
function USDShower(props: { amountSats: string, fee?: string }) {
|
||||
const [state, _] = useMegaStore()
|
||||
const amountInUsd = () => satsToUsd(state.price, add(props.amountSats, props.fee), true)
|
||||
|
||||
return (
|
||||
<Show when={!(props.amountSats === "0")}>
|
||||
<KeyValue gray key="">
|
||||
<div class="self-end">≈ {amountInUsd()} <span class="text-sm">USD</span></div>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
function add(a: string, b?: string) {
|
||||
return Number(a || 0) + Number(b || 0)
|
||||
}
|
||||
|
||||
export function AmountCard(props: { amountSats: string, fee?: string, initialOpen?: boolean, isAmountEditable?: boolean, setAmountSats?: (amount: bigint) => void }) {
|
||||
return (
|
||||
<Card>
|
||||
<VStack>
|
||||
<Switch>
|
||||
<Match when={props.fee}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Amount">
|
||||
<Show when={props.isAmountEditable} fallback={<InlineAmount amount={props.amountSats} />
|
||||
}>
|
||||
<AmountEditable initialOpen={props.initialOpen ?? false} initialAmountSats={props.amountSats.toString()} setAmountSats={props.setAmountSats ? props.setAmountSats : () => { }} />
|
||||
</Show>
|
||||
</KeyValue>
|
||||
<KeyValue gray key="+ Fee">
|
||||
<InlineAmount amount={props.fee || "0"} />
|
||||
</KeyValue>
|
||||
</div>
|
||||
<hr class="border-white/20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Total">
|
||||
<InlineAmount amount={add(props.amountSats, props.fee).toString()} />
|
||||
</KeyValue>
|
||||
<USDShower amountSats={props.amountSats} fee={props.fee} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!props.fee}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Amount">
|
||||
<Show when={props.isAmountEditable} fallback={<InlineAmount amount={props.amountSats} />
|
||||
}>
|
||||
<AmountEditable initialOpen={props.initialOpen ?? false} initialAmountSats={props.amountSats.toString()} setAmountSats={props.setAmountSats ? props.setAmountSats : () => { }} />
|
||||
</Show>
|
||||
</KeyValue>
|
||||
<USDShower amountSats={props.amountSats} />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</VStack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { For, Show, createMemo, createSignal } from 'solid-js';
|
||||
import { For, ParentComponent, Show, createMemo, 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';
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import pencil from "~/assets/icons/pencil.svg";
|
||||
import { InlineAmount } from './AmountCard';
|
||||
|
||||
const CHARACTERS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "DEL"];
|
||||
|
||||
@@ -25,8 +26,8 @@ function SingleDigitButton(props: { character: string, onClick: (c: string) => v
|
||||
);
|
||||
}
|
||||
|
||||
export function AmountEditable(props: { initialAmountSats: string, setAmountSats: (s: bigint) => void }) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
export const AmountEditable: ParentComponent<{ initialAmountSats: string, initialOpen: boolean, setAmountSats: (s: bigint) => void }> = (props) => {
|
||||
const [isOpen, setIsOpen] = createSignal(props.initialOpen);
|
||||
|
||||
const [displayAmount, setDisplayAmount] = createSignal(props.initialAmountSats || "0");
|
||||
|
||||
@@ -135,8 +136,13 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
||||
|
||||
return (
|
||||
<Dialog.Root isOpen={isOpen()}>
|
||||
<button onClick={() => setIsOpen(true)} class="p-4 rounded-xl border-2 border-m-blue">
|
||||
<Amount amountSats={Number(displayAmount())} showFiat />
|
||||
<button onClick={() => setIsOpen(true)} class="px-4 py-2 rounded-xl border-2 border-m-blue flex gap-2 items-center">
|
||||
{/* <Amount amountSats={Number(displayAmount())} showFiat /><span>✏️</span> */}
|
||||
<Show when={displayAmount() !== "0"} fallback={<div class="inline-block font-semibold">Set amount</div>}>
|
||||
<InlineAmount amount={displayAmount()} />
|
||||
</Show>
|
||||
<img src={pencil} alt="Edit" />
|
||||
{/* {props.children} */}
|
||||
</button>
|
||||
<Dialog.Portal>
|
||||
{/* <Dialog.Overlay class={OVERLAY} /> */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createResource, Show, Suspense } from "solid-js";
|
||||
import { ButtonLink, FancyCard } from "~/components/layout";
|
||||
import { Show, Suspense } from "solid-js";
|
||||
import { ButtonLink, FancyCard, Indicator } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Amount } from "./Amount";
|
||||
|
||||
@@ -10,12 +10,6 @@ function prettyPrintAmount(n?: number | bigint): string {
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
function SyncingIndicator() {
|
||||
return (
|
||||
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">Syncing</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BalanceBox() {
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
@@ -25,7 +19,7 @@ export default function BalanceBox() {
|
||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||
</FancyCard>
|
||||
|
||||
<FancyCard title="On-Chain" tag={state.is_syncing && <SyncingIndicator />}>
|
||||
<FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}>
|
||||
<div onClick={actions.sync}>
|
||||
<Amount amountSats={state.balance?.confirmed} showFiat />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Match, Switch, createSignal, createUniqueId } from 'solid-js';
|
||||
import { SmallHeader } from '~/components/layout';
|
||||
import { SmallHeader, TinyButton } from '~/components/layout';
|
||||
import { Dialog } from '@kobalte/core';
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import { SubmitHandler } from '@modular-forms/solid';
|
||||
@@ -39,7 +39,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={!props.list}>
|
||||
<button onClick={() => setIsOpen(true)} class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button>
|
||||
<TinyButton onClick={() => setIsOpen(true)}>+ Add Contact</TinyButton>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Dialog.Portal>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCopy } from "~/utils/useCopy";
|
||||
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
||||
const DIALOG_CONTENT = "max-w-[600px] max-h-full p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
const DIALOG_CONTENT = "max-w-[600px] max-h-full mx-4 p-4 bg-neutral-900/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
|
||||
export function JsonModal(props: { title: string, open: boolean, data?: unknown, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
||||
const json = createMemo(() => JSON.stringify(props.data, null, 2));
|
||||
|
||||
71
src/components/ShareCard.tsx
Normal file
71
src/components/ShareCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Card, VStack } from "~/components/layout";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
import copyIcon from "~/assets/icons/copy.svg"
|
||||
import shareIcon from "~/assets/icons/share.svg"
|
||||
import eyeIcon from "~/assets/icons/eye.svg"
|
||||
import { Show, createSignal } from "solid-js";
|
||||
import { JsonModal } from "./JsonModal";
|
||||
|
||||
const STYLE = "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold"
|
||||
|
||||
function ShareButton(props: { receiveString: string }) {
|
||||
async function share(receiveString: string) {
|
||||
// If the browser doesn't support share we can just copy the address
|
||||
if (!navigator.share) {
|
||||
console.error("Share not supported")
|
||||
}
|
||||
const shareData: ShareData = {
|
||||
title: "Mutiny Wallet",
|
||||
text: receiveString,
|
||||
}
|
||||
try {
|
||||
await navigator.share(shareData)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button class={STYLE} onClick={(_) => share(props.receiveString)}><span>Share</span><img src={shareIcon} alt="share" /></button>
|
||||
)
|
||||
}
|
||||
|
||||
export function StringShower(props: { text: string }) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
return (
|
||||
<>
|
||||
<JsonModal open={open()} data={props.text} title="Details" setOpen={setOpen} />
|
||||
<div class="flex gap-2">
|
||||
<pre class="truncate text-neutral-400">{props.text}</pre>
|
||||
<button class="w-[16rem]" onClick={() => setOpen(true)}>
|
||||
<img src={eyeIcon} alt="eye" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShareCard(props: { text?: string }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
function handleCopy() {
|
||||
copy(props.text ?? "")
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<StringShower text={props.text ?? ""} />
|
||||
<VStack>
|
||||
|
||||
<div class="flex gap-4 justify-center">
|
||||
<button class={STYLE} onClick={handleCopy}>{copied() ? "Copied" : "Copy"}<img src={copyIcon} alt="copy" /></button>
|
||||
<Show when={navigator.share}>
|
||||
<ShareButton receiveString={props.text ?? ""} />
|
||||
</Show>
|
||||
</div>
|
||||
</VStack>
|
||||
</Card >
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Select, createOptions } from "@thisbeyond/solid-select";
|
||||
import "~/styles/solid-select.css"
|
||||
import { SmallHeader } from "./layout";
|
||||
import { For, createUniqueId } from "solid-js";
|
||||
import { ContactEditor } from "./ContactEditor";
|
||||
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
|
||||
import { TinyButton } from "./layout";
|
||||
|
||||
// take two arrays, subtract the second from the first, then return the first
|
||||
function subtract<T>(a: T[], b: T[]) {
|
||||
const set = new Set(b);
|
||||
return a.filter(x => !set.has(x));
|
||||
};
|
||||
}
|
||||
|
||||
const createValue = (name: string): TextItem => {
|
||||
return { id: createUniqueId(), name, kind: "text" };
|
||||
};
|
||||
|
||||
export function TagEditor(props: { title: string, values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) {
|
||||
console.log(props.values);
|
||||
export function TagEditor(props: { values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) {
|
||||
const onChange = (selected: TagItem[]) => {
|
||||
props.setSelectedValues(selected);
|
||||
|
||||
@@ -42,7 +41,7 @@ export function TagEditor(props: { title: string, values: TagItem[], setValues:
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2 flex-grow flex-shrink flex-1" >
|
||||
<SmallHeader>{props.title}</SmallHeader>
|
||||
{/* FIXME this is causing overflow scroll for now good reason */}
|
||||
<Select
|
||||
multiple
|
||||
initialValue={props.selectedValues}
|
||||
@@ -53,15 +52,12 @@ export function TagEditor(props: { title: string, values: TagItem[], setValues:
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
|
||||
{(tag) => (
|
||||
<div onClick={() => onChange([...props.selectedValues, tag])}
|
||||
class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1"
|
||||
classList={{ "bg-black": tag.kind === "text", "bg-m-blue": tag.kind === "contact" && tag.color === "blue", "bg-m-green": tag.kind === "contact" && tag.color === "green", "bg-m-red": tag.kind === "contact" && tag.color === "red", "bg-[#898989]": tag.kind === "contact" && tag.color === "gray" }}
|
||||
<TinyButton onClick={() => onChange([...props.selectedValues, tag])}
|
||||
>
|
||||
{tag.name}
|
||||
</div>
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
{/* <button class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button> */}
|
||||
<ContactEditor createContact={newContact} />
|
||||
</div>
|
||||
</div >
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { JSX } from "solid-js";
|
||||
import { Button, LargeHeader, SmallHeader } from "~/components/layout";
|
||||
import { Button, LargeHeader } from "~/components/layout";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
|
||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
||||
const DIALOG_CONTENT = "h-full flex flex-col justify-between p-4 bg-gray/50 backdrop-blur-md bg-black/80"
|
||||
const DIALOG_CONTENT = "h-full flex flex-col justify-between p-4 backdrop-blur-md bg-neutral-900/50"
|
||||
|
||||
type FullscreenModalProps = {
|
||||
title: string,
|
||||
|
||||
@@ -4,13 +4,19 @@ import { For, Show } from "solid-js";
|
||||
type Choices = { value: string, label: string, caption: string }[]
|
||||
|
||||
// TODO: how could would it be if we could just pass the estimated fees in here?
|
||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, red?: boolean }) {
|
||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
|
||||
return (
|
||||
// TODO: rewrite this with CVA, props are bad for tailwind
|
||||
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)} class={`grid w-full gap-${props.small ? "2" : "4"} grid-cols-${props.choices.length.toString()}`}>
|
||||
<RadioGroup.Root value={props.value} onValueChange={(e) => props.onValueChange(e)}
|
||||
class={"grid w-full gap-4"}
|
||||
classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }}
|
||||
>
|
||||
<For each={props.choices}>
|
||||
{choice =>
|
||||
<RadioGroup.Item value={choice.value} class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ${props.red ? "ui-checked:outline-m-red" : "ui-checked:outline-m-blue"} ui-checked:outline-2`}>
|
||||
<RadioGroup.Item value={choice.value}
|
||||
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
|
||||
classList={{ "ui-checked:outline-m-red": props.accent === "red", "ui-checked:outline-white": props.accent === "white" }}
|
||||
>
|
||||
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
||||
<RadioGroup.ItemInput />
|
||||
<RadioGroup.ItemControl >
|
||||
|
||||
@@ -16,7 +16,7 @@ export const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
||||
|
||||
export const Card: ParentComponent<{ title?: string, titleElement?: JSX.Element }> = (props) => {
|
||||
return (
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden w-full'>
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950/50 overflow-x-hidden w-full'>
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.titleElement && props.titleElement}
|
||||
{props.children}
|
||||
@@ -97,7 +97,7 @@ export const Hr = () => <Separator.Root class="my-4 border-white/20" />
|
||||
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (props) => {
|
||||
return (
|
||||
<header class="w-full flex justify-between items-center mt-4 mb-2">
|
||||
<h1 class="text-4xl font-semibold">{props.children}</h1>
|
||||
<h1 class="text-3xl font-semibold">{props.children}</h1>
|
||||
<Show when={props.action}>
|
||||
{props.action}
|
||||
</Show>
|
||||
@@ -121,5 +121,16 @@ export const NiceP: ParentComponent = (props) => {
|
||||
return (<p class="text-2xl font-light">{props.children}</p>)
|
||||
}
|
||||
|
||||
export const TinyButton: ParentComponent<{ onClick: () => void }> = (props) => {
|
||||
return (
|
||||
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={props.onClick}>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const Indicator: ParentComponent = (props) => {
|
||||
return (
|
||||
<div class="box-border animate-pulse px-2 py-1 -my-1 bg-white/70 rounded text-xs uppercase text-black">{props.children}</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user