mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 14:54:26 +01:00
lightning details view
This commit is contained in:
3
src/assets/icons/bolt-black.svg
Normal file
3
src/assets/icons/bolt-black.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#000"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 921 B |
4
src/assets/icons/chain-black.svg
Normal file
4
src/assets/icons/chain-black.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#000"/>
|
||||||
|
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#000"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -9,6 +9,7 @@ import { getRedshifted } from '~/utils/fakeLabels';
|
|||||||
import { ActivityItem } from './ActivityItem';
|
import { ActivityItem } from './ActivityItem';
|
||||||
import { MutinyTagItem } from '~/utils/tags';
|
import { MutinyTagItem } from '~/utils/tags';
|
||||||
import { Network } from '~/logic/mutinyWalletSetup';
|
import { Network } from '~/logic/mutinyWalletSetup';
|
||||||
|
import { DetailsModal } from './DetailsModal';
|
||||||
|
|
||||||
export const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0'
|
export const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0'
|
||||||
export const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
|
export const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
|
||||||
@@ -53,7 +54,6 @@ function OnChainItem(props: { item: OnChainTx, labels: MutinyTagItem[], network:
|
|||||||
Mempool Link
|
Mempool Link
|
||||||
</a>
|
</a>
|
||||||
</JsonModal>
|
</JsonModal>
|
||||||
{/* {JSON.stringify(props.labels)} */}
|
|
||||||
<ActivityItem
|
<ActivityItem
|
||||||
kind={"onchain"}
|
kind={"onchain"}
|
||||||
labels={props.labels}
|
labels={props.labels}
|
||||||
@@ -74,7 +74,7 @@ function InvoiceItem(props: { item: MutinyInvoice, labels: MutinyTagItem[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
<DetailsModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
||||||
<ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
|
<ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ParentComponent, createMemo, createResource } from "solid-js";
|
import { ParentComponent, createMemo, createResource } from "solid-js";
|
||||||
import { InlineAmount } from "./AmountCard";
|
|
||||||
import { satsToUsd } from "~/utils/conversions";
|
import { satsToUsd } from "~/utils/conversions";
|
||||||
import bolt from "~/assets/icons/bolt.svg"
|
import bolt from "~/assets/icons/bolt.svg"
|
||||||
import chain from "~/assets/icons/chain.svg"
|
import chain from "~/assets/icons/chain.svg"
|
||||||
import { timeAgo } from "~/utils/prettyPrintTime";
|
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||||
import { MutinyTagItem } from "~/utils/tags";
|
import { MutinyTagItem } from "~/utils/tags";
|
||||||
import { generateGradient } from "~/utils/gradientHash";
|
import { generateGradient } from "~/utils/gradientHash";
|
||||||
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean }> = (props) => {
|
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean, center?: boolean }> = (props) => {
|
||||||
const amountInUsd = createMemo(() => {
|
const amountInUsd = createMemo(() => {
|
||||||
const parsed = Number(props.amount);
|
const parsed = Number(props.amount);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
@@ -27,7 +27,8 @@ export const ActivityAmount: ParentComponent<{ amount: string, price: number, po
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col items-end">
|
<div class="flex flex-col"
|
||||||
|
classList={{ "items-end": !props.center, "items-center": props.center }}>
|
||||||
<div class="text-base"
|
<div class="text-base"
|
||||||
classList={{ "text-m-green": props.positive }}
|
classList={{ "text-m-green": props.positive }}
|
||||||
>{props.positive && "+ "}{prettyPrint()} <span class="text-sm">SATS</span>
|
>{props.positive && "+ "}{prettyPrint()} <span class="text-sm">SATS</span>
|
||||||
@@ -74,6 +75,7 @@ function labelString(labels: MutinyTagItem[]) {
|
|||||||
|
|
||||||
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: MutinyTagItem[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
|
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: MutinyTagItem[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
|
||||||
const labels = () => sortLabels(props.labels)
|
const labels = () => sortLabels(props.labels)
|
||||||
|
const [state, _actions] = useMegaStore();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => props.onClick && props.onClick()}
|
onClick={() => props.onClick && props.onClick()}
|
||||||
@@ -93,7 +95,7 @@ export function ActivityItem(props: { kind: "lightning" | "onchain", labels: Mut
|
|||||||
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
|
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<ActivityAmount amount={props.amount.toString()} price={30000} positive={props.positive} />
|
<ActivityAmount amount={props.amount.toString()} price={state.price} positive={props.positive} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
142
src/components/DetailsModal.tsx
Normal file
142
src/components/DetailsModal.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { Dialog } from "@kobalte/core";
|
||||||
|
import { For, JSX, ParentComponent, Show, createMemo } from "solid-js";
|
||||||
|
import { Hr, TinyButton, VStack } from "~/components/layout";
|
||||||
|
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||||
|
import { OnChainTx } from "./Activity";
|
||||||
|
|
||||||
|
import eyeIcon from "~/assets/icons/eye.svg"
|
||||||
|
import close from "~/assets/icons/close.svg";
|
||||||
|
import bolt from "~/assets/icons/bolt-black.svg";
|
||||||
|
|
||||||
|
import { ActivityAmount } from "./ActivityItem";
|
||||||
|
import { CopyButton } from "./ShareCard";
|
||||||
|
import { prettyPrintTime } from "~/utils/prettyPrintTime";
|
||||||
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { tagToMutinyTag } from "~/utils/tags";
|
||||||
|
|
||||||
|
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-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||||
|
|
||||||
|
function LightningHeader(props: { info: MutinyInvoice }) {
|
||||||
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
|
const tags = createMemo(() => {
|
||||||
|
if (props.info.labels.length) {
|
||||||
|
let contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
|
||||||
|
if (contact) {
|
||||||
|
return [tagToMutinyTag(contact)]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<div class="p-4 bg-neutral-100 rounded-full">
|
||||||
|
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h1 class="uppercase font-semibold">{props.info.is_send ? "Lightning send" : "Lightning receive"}</h1>
|
||||||
|
<ActivityAmount center amount={props.info.amount_sats?.toString() ?? "0"} price={state.price} positive={!props.info.is_send} />
|
||||||
|
<For each={tags()}>
|
||||||
|
{(tag) => (
|
||||||
|
<TinyButton tag={tag} onClick={() => { }}>
|
||||||
|
{tag.name}
|
||||||
|
</TinyButton>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||||
|
return (
|
||||||
|
<li class="flex justify-between items-center gap-4">
|
||||||
|
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
|
||||||
|
{props.children}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniStringShower(props: { text: string }) {
|
||||||
|
return (
|
||||||
|
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
||||||
|
<pre class="truncate text-neutral-300">{props.text}</pre>
|
||||||
|
<button class="w-[1.5rem]" onClick={() => { }}>
|
||||||
|
<img src={eyeIcon} alt="eye" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LightningDetails(props: { info: MutinyInvoice }) {
|
||||||
|
return (
|
||||||
|
<VStack>
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
<KeyValue key="Status">
|
||||||
|
<span class="text-neutral-300">{props.info.paid ? "Paid" : "Unpaid"}</span>
|
||||||
|
</KeyValue>
|
||||||
|
<KeyValue key="When">
|
||||||
|
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
|
||||||
|
</KeyValue>
|
||||||
|
<Show when={props.info.description}>
|
||||||
|
<KeyValue key="Description">
|
||||||
|
<span class="text-neutral-300 truncate">{props.info.description}</span>
|
||||||
|
</KeyValue>
|
||||||
|
</Show>
|
||||||
|
<KeyValue key="Fees">
|
||||||
|
<span class="text-neutral-300">{props.info.fees_paid?.toLocaleString() ?? 0}</span>
|
||||||
|
</KeyValue>
|
||||||
|
<KeyValue key="Bolt11">
|
||||||
|
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||||
|
</KeyValue>
|
||||||
|
<KeyValue key="Payment Hash">
|
||||||
|
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||||
|
</KeyValue>
|
||||||
|
<KeyValue key="Preimage">
|
||||||
|
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||||
|
</KeyValue>
|
||||||
|
</ul>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DetailsModal(props: { title: string, open: boolean, data?: MutinyInvoice | OnChainTx, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
||||||
|
const json = createMemo(() => JSON.stringify(props.data, null, 2));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class={OVERLAY} />
|
||||||
|
<div class={DIALOG_POSITIONER}>
|
||||||
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
|
<div class="flex justify-between mb-2">
|
||||||
|
<div />
|
||||||
|
<Dialog.CloseButton>
|
||||||
|
<button tabindex="-1" class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue ">
|
||||||
|
<img src={close} alt="Close" class="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
</Dialog.CloseButton>
|
||||||
|
</div>
|
||||||
|
<Dialog.Title>
|
||||||
|
<LightningHeader info={props.data as MutinyInvoice} />
|
||||||
|
</Dialog.Title>
|
||||||
|
<Hr />
|
||||||
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
|
<LightningDetails info={props.data as MutinyInvoice} />
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<CopyButton title="Copy Json" text={json()} />
|
||||||
|
</div>
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root >
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { JsonModal } from "./JsonModal";
|
|||||||
|
|
||||||
const STYLE = "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold"
|
const STYLE = "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold"
|
||||||
|
|
||||||
function ShareButton(props: { receiveString: string }) {
|
export function ShareButton(props: { receiveString: string }) {
|
||||||
async function share(receiveString: string) {
|
async function share(receiveString: string) {
|
||||||
// If the browser doesn't support share we can just copy the address
|
// If the browser doesn't support share we can just copy the address
|
||||||
if (!navigator.share) {
|
if (!navigator.share) {
|
||||||
@@ -45,20 +45,25 @@ export function StringShower(props: { text: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareCard(props: { text?: string }) {
|
export function CopyButton(props: { text?: string, title?: string }) {
|
||||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
copy(props.text ?? "")
|
copy(props.text ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button class={STYLE} onClick={handleCopy}>{copied() ? "Copied" : props.title ?? "Copy"}<img src={copyIcon} alt="copy" /></button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareCard(props: { text?: string }) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<StringShower text={props.text ?? ""} />
|
<StringShower text={props.text ?? ""} />
|
||||||
<VStack>
|
<VStack>
|
||||||
|
|
||||||
<div class="flex gap-4 justify-center">
|
<div class="flex gap-4 justify-center">
|
||||||
<button class={STYLE} onClick={handleCopy}>{copied() ? "Copied" : "Copy"}<img src={copyIcon} alt="copy" /></button>
|
<CopyButton text={props.text ?? ""} />
|
||||||
<Show when={navigator.share}>
|
<Show when={navigator.share}>
|
||||||
<ShareButton receiveString={props.text ?? ""} />
|
<ShareButton receiveString={props.text ?? ""} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -39,8 +39,3 @@ a {
|
|||||||
#video-container .scan-region-highlight-svg {
|
#video-container .scan-region-highlight-svg {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Missing you sveltekit */
|
|
||||||
dd {
|
|
||||||
@apply mb-8 mt-2;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export function prettyPrintTime(ts: number) {
|
export function prettyPrintTime(ts: number) {
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
|||||||
Reference in New Issue
Block a user