new activity design

This commit is contained in:
Paul Miller
2023-05-09 16:44:59 -05:00
parent 9465eec52c
commit de83eef68a
8 changed files with 138 additions and 12 deletions

View 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

View 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

View File

@@ -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) {

View 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()}&nbsp;<span class="text-sm">SATS</span>
</div>
<div class="text-sm text-neutral-500">&#8776;&nbsp;{amountInUsd()}&nbsp;<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>
)
}

View File

@@ -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" />

View File

@@ -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">

View File

@@ -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" />

View File

@@ -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}`;
}
}