mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-21 08:14:27 +01:00
new activity design
This commit is contained in:
3
src/assets/icons/bolt.svg
Normal file
3
src/assets/icons/bolt.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="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 921 B |
4
src/assets/icons/chain.svg
Normal file
4
src/assets/icons/chain.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="#fff"/>
|
||||||
|
<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="#fff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -10,6 +10,7 @@ import mempoolTxUrl from '~/utils/mempoolTxUrl';
|
|||||||
import wave from "~/assets/wave.gif"
|
import wave from "~/assets/wave.gif"
|
||||||
import utxoIcon from '~/assets/icons/coin.svg';
|
import utxoIcon from '~/assets/icons/coin.svg';
|
||||||
import { getRedshifted } from '~/utils/fakeLabels';
|
import { getRedshifted } from '~/utils/fakeLabels';
|
||||||
|
import { ActivityItem } from './ActivityItem';
|
||||||
|
|
||||||
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'
|
||||||
@@ -57,7 +58,15 @@ function OnChainItem(props: { item: OnChainTx }) {
|
|||||||
Mempool Link
|
Mempool Link
|
||||||
</a>
|
</a>
|
||||||
</JsonModal>
|
</JsonModal>
|
||||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
<ActivityItem
|
||||||
|
kind={"onchain"}
|
||||||
|
labels={[]}
|
||||||
|
amount={isReceive() ? props.item.received : props.item.sent}
|
||||||
|
date={props.item.confirmation_time?.Confirmed?.time}
|
||||||
|
positive={isReceive()}
|
||||||
|
onClick={() => setOpen(!open())}
|
||||||
|
/>
|
||||||
|
{/* <div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}
|
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +80,7 @@ function OnChainItem(props: { item: OnChainTx }) {
|
|||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
<SubtleText>{props.item.confirmation_time?.Confirmed ? prettyPrintTime(props.item.confirmation_time?.Confirmed?.time) : "Unconfirmed"}</SubtleText>
|
<SubtleText>{props.item.confirmation_time?.Confirmed ? prettyPrintTime(props.item.confirmation_time?.Confirmed?.time) : "Unconfirmed"}</SubtleText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -84,7 +93,8 @@ function InvoiceItem(props: { item: MutinyInvoice }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
||||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
<ActivityItem kind={"lightning"} labels={[]} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
|
||||||
|
{/* <div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
|
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +108,7 @@ function InvoiceItem(props: { item: MutinyInvoice }) {
|
|||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
<SubtleText>{prettyPrintTime(Number(props.item.expire))}</SubtleText>
|
<SubtleText>{prettyPrintTime(Number(props.item.expire))}</SubtleText>
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div > */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -243,7 +253,9 @@ export function CombinedActivity(props: { limit?: number }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
invoices.forEach((invoice) => {
|
invoices.forEach((invoice) => {
|
||||||
|
if (invoice.paid) {
|
||||||
activity.push({ type: "lightning", item: invoice, time: Number(invoice.expire) })
|
activity.push({ type: "lightning", item: invoice, time: Number(invoice.expire) })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (props.limit) {
|
if (props.limit) {
|
||||||
|
|||||||
72
src/components/ActivityItem.tsx
Normal file
72
src/components/ActivityItem.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { ParentComponent, createMemo } from "solid-js";
|
||||||
|
import { InlineAmount } from "./AmountCard";
|
||||||
|
import { satsToUsd } from "~/utils/conversions";
|
||||||
|
import bolt from "~/assets/icons/bolt.svg"
|
||||||
|
import chain from "~/assets/icons/chain.svg"
|
||||||
|
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||||
|
|
||||||
|
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean }> = (props) => {
|
||||||
|
const amountInUsd = createMemo(() => {
|
||||||
|
const parsed = Number(props.amount);
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
return props.amount;
|
||||||
|
} else {
|
||||||
|
return satsToUsd(props.price, parsed, true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const prettyPrint = createMemo(() => {
|
||||||
|
const parsed = Number(props.amount);
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
return props.amount;
|
||||||
|
} else {
|
||||||
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<div class="text-base"
|
||||||
|
classList={{ "text-m-green": props.positive }}
|
||||||
|
>{props.positive && "+ "}{prettyPrint()} <span class="text-sm">SATS</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-neutral-500">≈ {amountInUsd()} <span class="text-sm">USD</span></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabelCircle(props: { name: string }) {
|
||||||
|
return (
|
||||||
|
<div class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
|
||||||
|
style={{ background: "gray" }}
|
||||||
|
>
|
||||||
|
{props.name[0] || "?"}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: string[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => props.onClick && props.onClick()}
|
||||||
|
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
|
||||||
|
classList={{ "cursor-pointer": !!props.onClick }}
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 md:gap-4 items-center">
|
||||||
|
<div class="">
|
||||||
|
{props.kind === "lightning" ? <img src={bolt} alt="lightning" /> : <img src={chain} alt="onchain" />}
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<LabelCircle name={props.labels.length ? props.labels[0] : "?"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-base font-semibold truncate" classList={{ "text-neutral-500": props.labels.length === 0 }}>{props.labels.length ? props.labels[0] : "Unknown"}</span>
|
||||||
|
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<ActivityAmount amount={props.amount.toString()} price={30000} positive={props.positive} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,11 +21,12 @@ export default function App() {
|
|||||||
<ReloadPrompt />
|
<ReloadPrompt />
|
||||||
<BalanceBox />
|
<BalanceBox />
|
||||||
<Card title="Activity">
|
<Card title="Activity">
|
||||||
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<CombinedActivity limit={3} />
|
<CombinedActivity limit={3} />
|
||||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
||||||
<A href="/activity" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center">View All</A>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
|
<A href="/activity" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline self-center">View All</A>
|
||||||
</Card>
|
</Card>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="home" />
|
<NavBar activeTab="home" />
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ export default function Activity() {
|
|||||||
<Tabs.Content value="mutiny">
|
<Tabs.Content value="mutiny">
|
||||||
{/* <MutinyActivity /> */}
|
{/* <MutinyActivity /> */}
|
||||||
<Card title="Activity">
|
<Card title="Activity">
|
||||||
|
<div class="p-1" />
|
||||||
|
<VStack>
|
||||||
<CombinedActivity />
|
<CombinedActivity />
|
||||||
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="nostr">
|
<Tabs.Content value="nostr">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { ActivityItem } from "~/components/ActivityItem";
|
||||||
import { AmountCard } from "~/components/AmountCard";
|
import { AmountCard } from "~/components/AmountCard";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
import { ShareCard } from "~/components/ShareCard";
|
import { ShareCard } from "~/components/ShareCard";
|
||||||
import { DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
|
import { Card, DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout";
|
||||||
|
|
||||||
const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6"
|
const SAMPLE = "bitcoin:tb1prqm8xtlgme0vmw5s30lgf0a4f5g4mkgsqundwmpu6thrg8zr6uvq2qrhzq?amount=0.001&lightning=lntbs1m1pj9n9xjsp5xgdrmvprtm67p7nq4neparalexlhlmtxx87zx6xeqthsplu842zspp546d6zd2seyaxpapaxx62m88yz3xueqtjmn9v6wj8y56np8weqsxqdqqnp4qdn2hj8tfknpuvdg6tz9yrf3e27ltrx9y58c24jh89lnm43yjwfc5xqrpwjcqpj9qrsgq5sdgh0m3ur5mu5hrmmag4mx9yvy86f83pd0x9ww80kgck6tac3thuzkj0mrtltaxwnlfea95h2re7tj4qsnwzxlvrdmyq2h9mgapnycpppz6k6"
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
@@ -11,10 +12,13 @@ export default function Admin() {
|
|||||||
<LargeHeader>Storybook</LargeHeader>
|
<LargeHeader>Storybook</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<AmountCard amountSats={"100000"} fee={"69"} />
|
<AmountCard amountSats={"100000"} fee={"69"} />
|
||||||
<AmountCard amountSats={"100000"} />
|
|
||||||
<AmountCard amountSats={"100000"} isAmountEditable />
|
|
||||||
<AmountCard amountSats={"0"} isAmountEditable />
|
|
||||||
<ShareCard text={SAMPLE} />
|
<ShareCard text={SAMPLE} />
|
||||||
|
<Card title="Activity">
|
||||||
|
<ActivityItem kind="lightning" name="benthecarman" amount={100000} date={1683664966} />
|
||||||
|
<ActivityItem kind="onchain" name="tony" amount={42000000} positive date={1683664966} />
|
||||||
|
<ActivityItem kind="onchain" name="a fake name that is too long" amount={42000000} date={1683664966} />
|
||||||
|
<ActivityItem kind="onchain" name="a fake name that is too long" amount={42000000} date={1683664966} />
|
||||||
|
</Card>
|
||||||
</VStack>
|
</VStack>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="none" />
|
<NavBar activeTab="none" />
|
||||||
|
|||||||
@@ -10,3 +10,30 @@ export function prettyPrintTime(ts: number) {
|
|||||||
|
|
||||||
return new Date(ts * 1000).toLocaleString('en-US', options);
|
return new Date(ts * 1000).toLocaleString('en-US', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timeAgo(ts?: number | bigint): string {
|
||||||
|
if (!ts || ts === 0) return "Pending";
|
||||||
|
const timestamp = Number(ts) * 1000;
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedMilliseconds = now - timestamp;
|
||||||
|
const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);
|
||||||
|
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
||||||
|
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
||||||
|
const elapsedDays = Math.floor(elapsedHours / 24);
|
||||||
|
|
||||||
|
if (elapsedSeconds < 60) {
|
||||||
|
return "Just now";
|
||||||
|
} else if (elapsedMinutes < 60) {
|
||||||
|
return `${elapsedMinutes} minute${elapsedMinutes > 1 ? 's' : ''} ago`;
|
||||||
|
} else if (elapsedHours < 24) {
|
||||||
|
return `${elapsedHours} hour${elapsedHours > 1 ? 's' : ''} ago`;
|
||||||
|
} else if (elapsedDays < 7) {
|
||||||
|
return `${elapsedDays} day${elapsedDays > 1 ? 's' : ''} ago`;
|
||||||
|
} else {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
return `${month}/${day}/${year}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user