mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 23:04:25 +01:00
onchain, lightning, and utxo activity cards
This commit is contained in:
3
src/assets/icons/receive.svg
Normal file
3
src/assets/icons/receive.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.333 13.008a.667.667 0 0 1-.666.667h-6A.667.667 0 0 1 3 13.008v-6a.667.667 0 1 1 1.333 0v4.39l7.348-7.346a.667.667 0 1 1 .942.942l-7.347 7.348h4.39c.369 0 .667.298.667.666Z" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
204
src/components/Activity.tsx
Normal file
204
src/components/Activity.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import send from '~/assets/icons/send.svg';
|
||||||
|
import receive from '~/assets/icons/receive.svg';
|
||||||
|
import { Card, Hr, LoadingSpinner, SmallAmount, SmallHeader, VStack } from './layout';
|
||||||
|
import { For, JSX, Match, Show, Suspense, Switch, createMemo, createResource, createSignal } from 'solid-js';
|
||||||
|
import { useMegaStore } from '~/state/megaStore';
|
||||||
|
import { MutinyInvoice } from '@mutinywallet/mutiny-wasm';
|
||||||
|
import { prettyPrintTime } from '~/utils/prettyPrintTime';
|
||||||
|
import { JsonModal } from './JsonModal';
|
||||||
|
import mempoolTxUrl from '~/utils/mempoolTxUrl';
|
||||||
|
|
||||||
|
const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0'
|
||||||
|
const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
|
||||||
|
const MISSING_LABEL = 'py-1 px-2 bg-m-red rounded inline-block text-sm'
|
||||||
|
const RIGHT_COLUMN = 'flex flex-col items-right text-right'
|
||||||
|
|
||||||
|
type OnChainTx = {
|
||||||
|
txid: string
|
||||||
|
received: number
|
||||||
|
sent: number
|
||||||
|
fee?: number
|
||||||
|
confirmation_time?: {
|
||||||
|
height: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Utxo = {
|
||||||
|
outpoint: string
|
||||||
|
txout: {
|
||||||
|
value: number
|
||||||
|
script_pubkey: string
|
||||||
|
}
|
||||||
|
keychain: string
|
||||||
|
is_spent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubtleText(props: { children: any }) {
|
||||||
|
return <h3 class='text-xs text-gray-500 uppercase'>{props.children}</h3>
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnChainItem(props: { item: OnChainTx }) {
|
||||||
|
const isReceive = createMemo(() => props.item.received > 0);
|
||||||
|
|
||||||
|
const [open, setOpen] = createSignal(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonModal open={open()} data={props.item} title="On-Chain Transaction" setOpen={setOpen}>
|
||||||
|
<a href={mempoolTxUrl(props.item.txid, "signet")} target="_blank" rel="noreferrer">
|
||||||
|
Mempool Link
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</JsonModal>
|
||||||
|
<div class={THREE_COLUMNS} onclick={() => setOpen(!open())}>
|
||||||
|
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}
|
||||||
|
<div class={CENTER_COLUMN}>
|
||||||
|
<h2 class={MISSING_LABEL}>Label Missing</h2>
|
||||||
|
{isReceive() ? <SmallAmount amount={props.item.received} /> : <SmallAmount amount={props.item.sent} />}
|
||||||
|
{/* <h2 class="truncate">Txid: {props.item.txid}</h2> */}
|
||||||
|
</div>
|
||||||
|
<div class={RIGHT_COLUMN}>
|
||||||
|
<SmallHeader class={isReceive() ? "text-m-green" : "text-m-red"}>
|
||||||
|
{isReceive() ? "RECEIVE" : "SEND"}
|
||||||
|
</SmallHeader>
|
||||||
|
<SubtleText>{props.item.confirmation_time ? prettyPrintTime(props.item.confirmation_time.timestamp) : "Unconfirmed"}</SubtleText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InvoiceItem(props: { item: MutinyInvoice }) {
|
||||||
|
const isSend = createMemo(() => props.item.is_send);
|
||||||
|
|
||||||
|
const [open, setOpen] = createSignal(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
||||||
|
<div class={THREE_COLUMNS} onclick={() => setOpen(!open())}>
|
||||||
|
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
|
||||||
|
<div class={CENTER_COLUMN}>
|
||||||
|
<h2 class={MISSING_LABEL}>Label Missing</h2>
|
||||||
|
<SmallAmount amount={props.item.amount_sats || 0} />
|
||||||
|
</div>
|
||||||
|
<div class={RIGHT_COLUMN}>
|
||||||
|
<SmallHeader class={isSend() ? "text-m-red" : "text-m-green"}>
|
||||||
|
{isSend() ? "SEND" : "RECEIVE"}
|
||||||
|
</SmallHeader>
|
||||||
|
<SubtleText>{prettyPrintTime(Number(props.item.expire))}</SubtleText>
|
||||||
|
</div>
|
||||||
|
</div >
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Utxo(props: { item: Utxo }) {
|
||||||
|
const spent = createMemo(() => props.item.is_spent);
|
||||||
|
|
||||||
|
const [open, setOpen] = createSignal(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonModal open={open()} data={props.item} title="Unspent Transaction Output" setOpen={setOpen} />
|
||||||
|
<div class={THREE_COLUMNS} onclick={() => setOpen(!open())}>
|
||||||
|
<img src={receive} alt="receive arrow" />
|
||||||
|
<div class={CENTER_COLUMN}>
|
||||||
|
<h2 class={MISSING_LABEL}>Label Missing</h2>
|
||||||
|
<SmallAmount amount={props.item.txout.value} />
|
||||||
|
</div>
|
||||||
|
<div class={RIGHT_COLUMN}>
|
||||||
|
<SmallHeader class={spent() ? "text-m-red" : "text-m-green"}>
|
||||||
|
{spent() ? "SPENT" : "UNSPENT"}
|
||||||
|
</SmallHeader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Activity() {
|
||||||
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
|
const getTransactions = async () => {
|
||||||
|
console.log("Getting onchain txs");
|
||||||
|
const txs = await state.node_manager?.list_onchain() as OnChainTx[];
|
||||||
|
return txs.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInvoices = async () => {
|
||||||
|
console.log("Getting invoices");
|
||||||
|
const invoices = await state.node_manager?.list_invoices() as MutinyInvoice[];
|
||||||
|
return invoices.filter((inv) => inv.paid).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUtXos = async () => {
|
||||||
|
console.log("Getting utxos");
|
||||||
|
const utxos = await state.node_manager?.list_utxos() as Utxo[];
|
||||||
|
return utxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [transactions, { refetch: refetchTransactions }] = createResource(getTransactions);
|
||||||
|
const [invoices, { refetch: refetchInvoices }] = createResource(getInvoices);
|
||||||
|
const [utxos, { refetch: refetchUtxos }] = createResource(getUtXos);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack>
|
||||||
|
<Suspense>
|
||||||
|
<Card title="On-chain">
|
||||||
|
<Switch>
|
||||||
|
<Match when={transactions.loading}>
|
||||||
|
<LoadingSpinner big />
|
||||||
|
</Match>
|
||||||
|
<Match when={transactions.state === "ready" && transactions().length === 0}>
|
||||||
|
<code>No transactions (empty state)</code>
|
||||||
|
</Match>
|
||||||
|
<Match when={transactions.state === "ready" && transactions().length >= 0}>
|
||||||
|
<For each={transactions()}>
|
||||||
|
{(tx) =>
|
||||||
|
<OnChainItem item={tx} />
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
<Card title="Lightning">
|
||||||
|
<Switch>
|
||||||
|
<Match when={invoices.loading}>
|
||||||
|
<LoadingSpinner big />
|
||||||
|
</Match>
|
||||||
|
<Match when={invoices.state === "ready" && invoices().length === 0}>
|
||||||
|
<code>No invoices (empty state)</code>
|
||||||
|
</Match>
|
||||||
|
<Match when={invoices.state === "ready" && invoices().length >= 0}>
|
||||||
|
<For each={invoices()}>
|
||||||
|
{(invoice) =>
|
||||||
|
<InvoiceItem item={invoice} />
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
<Card title="UTXOs">
|
||||||
|
<Switch>
|
||||||
|
<Match when={utxos.loading}>
|
||||||
|
<LoadingSpinner big />
|
||||||
|
</Match>
|
||||||
|
<Match when={utxos.state === "ready" && utxos().length === 0}>
|
||||||
|
<code>No utxos (empty state)</code>
|
||||||
|
</Match>
|
||||||
|
<Match when={utxos.state === "ready" && utxos().length >= 0}>
|
||||||
|
<For each={utxos()}>
|
||||||
|
{(utxo) =>
|
||||||
|
<Utxo item={utxo} />
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
</Suspense>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,12 +12,7 @@ function prettyPrintAmount(n?: number | bigint): string {
|
|||||||
export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean, loading?: boolean }) {
|
export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean, loading?: boolean }) {
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore()
|
||||||
|
|
||||||
async function getPrice() {
|
const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true)
|
||||||
return await state.node_manager?.get_bitcoin_price()
|
|
||||||
}
|
|
||||||
|
|
||||||
const [price] = createResource(getPrice)
|
|
||||||
const amountInUsd = () => satsToUsd(price(), Number(props.amountSats) || 0, true)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -98,12 +98,7 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
|||||||
// Fiat conversion
|
// Fiat conversion
|
||||||
const [state, _] = useMegaStore()
|
const [state, _] = useMegaStore()
|
||||||
|
|
||||||
async function getPrice() {
|
const amountInUsd = () => satsToUsd(state.price, Number(displayAmount()) || 0, true)
|
||||||
return await state.node_manager?.get_bitcoin_price()
|
|
||||||
}
|
|
||||||
|
|
||||||
const [price] = createResource(getPrice)
|
|
||||||
const amountInUsd = () => satsToUsd(price(), Number(displayAmount()) || 0, true)
|
|
||||||
|
|
||||||
// What we're all here for in the first place: returning a value
|
// What we're all here for in the first place: returning a value
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import NavBar from "~/components/NavBar";
|
|||||||
import ReloadPrompt from "~/components/Reload";
|
import ReloadPrompt from "~/components/Reload";
|
||||||
import { Scan } from '~/assets/svg/Scan';
|
import { Scan } from '~/assets/svg/Scan';
|
||||||
import { A } from 'solid-start';
|
import { A } from 'solid-start';
|
||||||
|
import { Activity } from './Activity';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -15,8 +16,9 @@ export default function App() {
|
|||||||
<img src={logo} class="h-10" alt="logo" />
|
<img src={logo} class="h-10" alt="logo" />
|
||||||
<A class="p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="scanner"><Scan /></A>
|
<A class="p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="scanner"><Scan /></A>
|
||||||
</header>
|
</header>
|
||||||
<BalanceBox />
|
|
||||||
<ReloadPrompt />
|
<ReloadPrompt />
|
||||||
|
<BalanceBox />
|
||||||
|
<Activity />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="home" />
|
<NavBar activeTab="home" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
|
|||||||
44
src/components/JsonModal.tsx
Normal file
44
src/components/JsonModal.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Dialog } from "@kobalte/core";
|
||||||
|
import { JSX, createMemo } from "solid-js";
|
||||||
|
import { Button, ButtonLink, SmallHeader } from "~/components/layout";
|
||||||
|
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-screen-safe p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10 overflow-y-scroll disable-scrollbars"
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root isOpen={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">
|
||||||
|
<Dialog.Title>
|
||||||
|
<SmallHeader>
|
||||||
|
{props.title}
|
||||||
|
</SmallHeader>
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.CloseButton>
|
||||||
|
<code>X</code>
|
||||||
|
</Dialog.CloseButton>
|
||||||
|
</div>
|
||||||
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
|
<pre class="whitespace-pre-wrap break-all">
|
||||||
|
{json()}
|
||||||
|
</pre>
|
||||||
|
{props.children}
|
||||||
|
<Button onClick={(_) => copy(json() ?? "")}>{copied() ? "Copied" : "Copy"}</Button>
|
||||||
|
<Button onClick={(_) => props.setOpen(false)}>Close</Button>
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root >
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ function PeersList() {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<For each={peers()} fallback={<code>No peers</code>}>
|
<For each={peers()} fallback={<code>No peers</code>}>
|
||||||
{(peer) => (
|
{(peer) => (
|
||||||
<pre class="overflow-x-auto whitespace-pre-line break-all">
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
{JSON.stringify(peer, null, 2)}
|
{JSON.stringify(peer, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -102,7 +102,7 @@ function ChannelsList() {
|
|||||||
<For each={channels()} fallback={<code>No channels</code>}>
|
<For each={channels()} fallback={<code>No channels</code>}>
|
||||||
{(channel) => (
|
{(channel) => (
|
||||||
<>
|
<>
|
||||||
<pre class="overflow-x-auto whitespace-pre-line break-all">
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
{JSON.stringify(channel, null, 2)}
|
{JSON.stringify(channel, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(channel.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
|
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(channel.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
|
||||||
@@ -182,7 +182,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
</form >
|
</form >
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
<Show when={newChannel()}>
|
<Show when={newChannel()}>
|
||||||
<pre class="overflow-x-auto whitespace-pre-line break-all">
|
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<pre>{newChannel()?.outpoint}</pre>
|
<pre>{newChannel()?.outpoint}</pre>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
|||||||
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
const DIALOG_CONTENT = "w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||||
|
|
||||||
export function SentModal(props: { details?: { nice: string } }) {
|
export function SentModal(props: { details?: { nice: string } }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root isOpen={!!props.details}>
|
<Dialog.Root isOpen={!!props.details}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { Button, ButtonLink } from "./Button"
|
|||||||
import { Separator } from "@kobalte/core"
|
import { Separator } from "@kobalte/core"
|
||||||
import { useMegaStore } from "~/state/megaStore"
|
import { useMegaStore } from "~/state/megaStore"
|
||||||
|
|
||||||
const SmallHeader: ParentComponent = (props) => <header class='text-sm font-semibold uppercase'>{props.children}</header>
|
const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
||||||
|
return <header class={`text-sm font-semibold uppercase ${props.class}`}>{props.children}</header>
|
||||||
|
}
|
||||||
|
|
||||||
const Card: ParentComponent<{ title?: string }> = (props) => {
|
const Card: ParentComponent<{ title?: string }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-800'>
|
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950'>
|
||||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
@@ -74,8 +76,8 @@ const NodeManagerGuard: ParentComponent = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingSpinner = () => {
|
const LoadingSpinner = (props: { big?: boolean }) => {
|
||||||
return (<div role="status" class="w-full h-full grid" >
|
return (<div role="status" class={props.big ? "w-full h-full grid" : ""} >
|
||||||
<svg aria-hidden="true" class="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-m-red place-self-center" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg aria-hidden="true" class="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-m-red place-self-center" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||||
@@ -90,4 +92,29 @@ const LargeHeader: ParentComponent = (props) => {
|
|||||||
return (<h1 class="text-4xl font-semibold uppercase border-b-2 border-b-white my-4">{props.children}</h1>)
|
return (<h1 class="text-4xl font-semibold uppercase border-b-2 border-b-white my-4">{props.children}</h1>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SmallHeader, Card, SafeArea, LoadingSpinner, Button, ButtonLink, Linkify, Hr, NodeManagerGuard, FullscreenLoader, InnerCard, FancyCard, DefaultMain, LargeHeader }
|
const VStack: ParentComponent = (props) => {
|
||||||
|
return (<div class="flex flex-col gap-4">{props.children}</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SmallAmount: ParentComponent<{ amount: number | bigint }> = (props) => {
|
||||||
|
return (<h2 class="font-light text-lg">{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
SmallHeader,
|
||||||
|
Card,
|
||||||
|
SafeArea,
|
||||||
|
LoadingSpinner,
|
||||||
|
Button,
|
||||||
|
ButtonLink,
|
||||||
|
Linkify,
|
||||||
|
Hr,
|
||||||
|
NodeManagerGuard,
|
||||||
|
FullscreenLoader,
|
||||||
|
InnerCard,
|
||||||
|
FancyCard,
|
||||||
|
DefaultMain,
|
||||||
|
LargeHeader,
|
||||||
|
VStack,
|
||||||
|
SmallAmount
|
||||||
|
}
|
||||||
|
|||||||
20
src/routes/Admin.tsx
Normal file
20
src/routes/Admin.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Activity } from "~/components/Activity";
|
||||||
|
import KitchenSink from "~/components/KitchenSink";
|
||||||
|
import NavBar from "~/components/NavBar";
|
||||||
|
import { Card, DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
|
||||||
|
|
||||||
|
export default function Admin() {
|
||||||
|
return (
|
||||||
|
<SafeArea>
|
||||||
|
<DefaultMain>
|
||||||
|
<LargeHeader>Admin</LargeHeader>
|
||||||
|
<VStack>
|
||||||
|
<Card><p>If you know what you're doing you're in the right place!</p></Card>
|
||||||
|
<Activity />
|
||||||
|
<KitchenSink />
|
||||||
|
</VStack>
|
||||||
|
</DefaultMain>
|
||||||
|
<NavBar activeTab="none" />
|
||||||
|
</SafeArea>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -82,14 +82,7 @@ export default function Receive() {
|
|||||||
setReceiveState("show")
|
setReceiveState("show")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPrice() {
|
const amountInUsd = createMemo(() => satsToUsd(state.price, parseInt(amount()) || 0, true))
|
||||||
// return await state.node_manager?.get_bitcoin_price()
|
|
||||||
return 30000
|
|
||||||
}
|
|
||||||
|
|
||||||
const [price] = createResource(getPrice)
|
|
||||||
|
|
||||||
const amountInUsd = createMemo(() => satsToUsd(price(), parseInt(amount()) || 0, true))
|
|
||||||
|
|
||||||
function handleAmountSave() {
|
function handleAmountSave() {
|
||||||
console.error("focusing label input...")
|
console.error("focusing label input...")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type MegaStore = [{
|
|||||||
scan_result?: string;
|
scan_result?: string;
|
||||||
balance?: MutinyBalance;
|
balance?: MutinyBalance;
|
||||||
last_sync?: number;
|
last_sync?: number;
|
||||||
|
price: number
|
||||||
}, {
|
}, {
|
||||||
fetchUserStatus(): Promise<UserStatus>;
|
fetchUserStatus(): Promise<UserStatus>;
|
||||||
setupNodeManager(): Promise<void>;
|
setupNodeManager(): Promise<void>;
|
||||||
@@ -27,6 +28,8 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
waitlist_id: localStorage.getItem("waitlist_id"),
|
waitlist_id: localStorage.getItem("waitlist_id"),
|
||||||
node_manager: undefined as NodeManager | undefined,
|
node_manager: undefined as NodeManager | undefined,
|
||||||
user_status: undefined as UserStatus,
|
user_status: undefined as UserStatus,
|
||||||
|
// TODO: wire this up to real price once we have caching
|
||||||
|
price: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
|||||||
12
src/utils/prettyPrintTime.ts
Normal file
12
src/utils/prettyPrintTime.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export function prettyPrintTime(ts: number) {
|
||||||
|
const options = {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric'
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Date(ts * 1000).toLocaleString('en-US', options as any);
|
||||||
|
}
|
||||||
@@ -70,6 +70,9 @@ module.exports = {
|
|||||||
'.min-h-screen-safe': {
|
'.min-h-screen-safe': {
|
||||||
minHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
minHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||||
},
|
},
|
||||||
|
'.max-h-screen-safe': {
|
||||||
|
maxHeight: 'calc(100vh - (env(safe-area-inset-top) + env(safe-area-inset-bottom)))'
|
||||||
|
},
|
||||||
'.disable-scrollbars': {
|
'.disable-scrollbars': {
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
'-ms-overflow-style': 'none',
|
'-ms-overflow-style': 'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user