aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-12-24 00:41:51 -0800
committerAdam Soltys <[email protected]>2024-12-24 00:41:51 -0800
commit3c0489e1cca4f6a211215b700489d256695c3ce1 (patch)
treed477237d739237e2ca527750be2cd139393fb36a /src
parent747e8d82fbfa88351a99e8d5cb5712ba4b6e4000 (diff)
downloadcoinos-ui-3c0489e1cca4f6a211215b700489d256695c3ce1.tar.gz
coinos-ui-3c0489e1cca4f6a211215b700489d256695c3ce1.zip
create fund
Diffstat (limited to 'src')
-rw-r--r--src/components/Lnurlw.svelte6
-rw-r--r--src/components/Payments.svelte120
-rw-r--r--src/locales/en.json23
-rw-r--r--src/routes/(app)/fund/[id]/+page.server.ts7
-rw-r--r--src/routes/(app)/fund/[id]/+page.svelte137
-rw-r--r--src/routes/(app)/fund/[id]/withdraw/+page.server.ts15
-rw-r--r--src/routes/(app)/fund/[id]/withdraw/+page.svelte59
-rw-r--r--src/routes/(app)/ln/[lnurl]/+page.server.ts9
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte6
-rw-r--r--src/routes/(app)/payments/[...page]/+page.svelte110
-rw-r--r--src/routes/(app)/qr/[text]/+page.svelte2
-rw-r--r--src/routes/(app)/send/[...text]/+page.svelte8
-rw-r--r--src/routes/(app)/send/fund/[...name]/[...amount]/+page.server.ts28
-rw-r--r--src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte70
-rw-r--r--src/routes/(app)/send/fund/[name]/[...amount]/+page.server.ts17
-rw-r--r--src/routes/(app)/send/fund/[name]/[...amount]/+page.svelte66
16 files changed, 350 insertions, 333 deletions
diff --git a/src/components/Lnurlw.svelte b/src/components/Lnurlw.svelte
index e3345d31..8793d3b4 100644
--- a/src/components/Lnurlw.svelte
+++ b/src/components/Lnurlw.svelte
@@ -55,7 +55,7 @@
<Numpad bind:amount {currency} {rate} />
{/if}
- <form action="?/withdraw" method="POST" use:enhance onsubmit={submit}>
+ <form action="?/withdraw" method="POST" use:enhance onsubmit={submit} class="space-y-5">
<input name="amount" value={amount} type="hidden" />
<input name="username" value={username} type="hidden" />
<input name="currency" value={currency} type="hidden" />
@@ -65,11 +65,11 @@
<input name="k1" value={k1} type="hidden" />
<div class="flex w-full">
- <button type="submit" class="btn">
+ <button type="submit" class="btn btn-accent">
{#if loading}
<Spinner />
{:else}
- {$t("payments.redeem")}
+ {$t("payments.withdraw")}
{/if}
</button>
</div>
diff --git a/src/components/Payments.svelte b/src/components/Payments.svelte
new file mode 100644
index 00000000..2d38513e
--- /dev/null
+++ b/src/components/Payments.svelte
@@ -0,0 +1,120 @@
+<script>
+ import { goto } from "$app/navigation";
+ import Avatar from "$comp/Avatar.svelte";
+ import { get, f, s, si, sat, loc, sats, types } from "$lib/utils";
+ import { format } from "date-fns";
+ import { t } from "$lib/translations";
+
+ const { fund, locale, payments } = $props();
+</script>
+
+<div class="text-base">
+ {#each payments as p, i}
+ {@const amount = fund ? -p.amount : p.amount}
+ <div
+ class="grid grid-cols-12 border-b border-base-200 hover:bg-base-200 px-1 py-2 lg:p-4 cursor-pointer"
+ class:border-b-0={i === payments.length - 1}
+ class:text-error={amount < 0}
+ onclick={() => goto(`/payment/${p.id}`)}
+ >
+ <div class="whitespace-nowrap my-auto col-span-3">
+ <div class="font-bold flex items-center">
+ <div class="flex items-center">
+ <iconify-icon
+ icon="ph:lightning-fill"
+ width="24"
+ class="text-yellow-300"
+ ></iconify-icon>
+ <div>{s(Math.abs(amount), locale)}</div>
+ </div>
+ </div>
+
+ <div class="text-secondary flex items-center text-base">
+ {f(Math.abs(amount) * (p.rate / sats), p.currency, locale)}
+
+ {#if p.tip}
+ <span class="text-sm text-secondary">
+ &nbsp;+{Math.round((p.tip / Math.abs(amount)) * 100)}%
+ </span>
+ {/if}
+ </div>
+ </div>
+
+ <div
+ class="flex my-auto col-span-6 truncate text-ellipsis overflow-hidden mx-auto"
+ >
+ {#if p.type === types.fund}
+ {#if fund}
+ <div class="flex">
+ <div class="my-auto">
+ <Avatar user={p.user} size={12} disabled={true} />
+ </div>
+ <div class="my-auto ml-1 text-secondary">
+ {p.user.username}
+ </div>
+ </div>
+ {:else}
+ <a href={`/fund/${p.memo}`}>
+ <div class="text-secondary flex">
+ <iconify-icon
+ icon="ph:lightning-fill"
+ class="text-yellow-300 text-3xl"
+ ></iconify-icon>
+
+ <div class="my-auto">{$t("payments.fund")}</div>
+ </div>
+ </a>
+ {/if}
+ {:else if p.with}
+ <div class="flex">
+ <div class="my-auto">
+ <Avatar user={p.with} size={12} disabled={true} />
+ </div>
+ <div class="my-auto ml-1 text-secondary">
+ {p.with.username}
+ </div>
+ </div>
+ {:else}
+ <div class="text-secondary flex items-center gap-1">
+ {#if p.type === types.lightning}
+ <iconify-icon
+ icon="ph:lightning-fill"
+ class="text-yellow-300 text-3xl"
+ ></iconify-icon>
+ {:else if p.type === types.ecash}
+ <img src="/images/cash.png" class="w-12" />
+ {:else if p.type === types.reconcile}
+ <iconify-icon icon="ph:scales-bold" width="32"></iconify-icon>
+ {:else if p.type === types.bitcoin}
+ <iconify-icon icon="logos:bitcoin" class="text-3xl"
+ ></iconify-icon>
+ {:else if p.type === types.liquid}
+ <div class="my-auto">
+ <img
+ src="/images/liquid.svg"
+ class="w-10 border-4 border-transparent"
+ alt="Liquid"
+ />
+ </div>
+ {/if}
+
+ <div class="my-auto">
+ {amount > 0 ? (p.confirmed ? "Received" : "Pending") : "Sent"}
+ </div>
+ </div>
+ {/if}
+ </div>
+
+ <div class="text-secondary text-right text-sm my-auto col-span-3">
+ <div>
+ {format(new Date(p.created), "h:mm aaa", { locale })}
+ </div>
+ <div>
+ {format(new Date(p.created), "MMM d, yy", { locale })}
+ </div>
+ </div>
+ </div>
+ {:else}
+ <p class="text-secondary text-lg text-center">{$t("payments.empty")}</p>
+ {/each}
+</div>
diff --git a/src/locales/en.json b/src/locales/en.json
index 26f9779b..a0d0f855 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -111,7 +111,7 @@
"addTipq": "Add a tip?",
"addMemo": "Add memo",
"setMemo": "Set memo",
- "memo": "Memo",
+ "memo": "Memo",
"done": "Done",
"paymentSuccessful": "Payment received!",
"invoice": "Invoice",
@@ -193,7 +193,7 @@
"subheader": "Latest Updates"
},
"user": {
- "loadMore": "Load more",
+ "loadMore": "Load more",
"url": "Public URL",
"install": "Install app",
"moreDetails": "Profile details",
@@ -250,8 +250,8 @@
"placeholder": "Enter an address, invoice or username"
},
"settings": {
- "integrations": "Integrations",
- "connectNostr": "Nostr Wallet Connect",
+ "integrations": "Integrations",
+ "connectNostr": "Nostr Wallet Connect",
"pleaseEnterYourPassword": "Please enter your password",
"memoPrompt": "Checkout instructions",
"memoPromptPlaceholder": "e.g. Please enter your name for the order",
@@ -262,7 +262,7 @@
"destinationPlaceholder": "Enter a Bitcoin, Liquid or Lightning address",
"reserve": "Reserve amount",
"reserveDesc": "The amount to be left in your account after the withdrawal is complete",
- "nostr": "Nostr",
+ "nostr": "Nostr",
"shopify": "Shopify",
"threshold": "Autowithdrawal threshold",
"thresholdDesc": "When your balance exceeds this amount, a withdrawal will be triggered",
@@ -276,8 +276,8 @@
"seedPhrase": "Seed phrase",
"nostrKeys": "Nostr secret key",
"nostrDescription": "Your nostr secret key is used to sign and decrypt messages, keep it safe or someone may be able to impersonate you.",
- "nwc": "Nostr wallet connect",
- "nwcDescription": "Enable your nostr app to send payments from your account",
+ "nwc": "Nostr wallet connect",
+ "nwcDescription": "Enable your nostr app to send payments from your account",
"seedDescription": "This 12 word seed phrase is used to derive your nostr private key and your self-custodial wallet keys which determine your auto-withdrawal addresses. Back it up and don't let anyone see it.",
"emailDescription": "For password resets and payment notifications only",
"hideNsec": "Hide nostr secret key",
@@ -361,6 +361,7 @@
"memo": "Notes",
"voucherText": "Enter an ecash token or lnurlw code",
"createEcash": "Mint ecash",
+ "lnurlw": "LNURLw",
"tips": "Tips",
"options": "options",
"bump": "Bump fee",
@@ -368,6 +369,8 @@
"platformFee": "Platform fee",
"fastest": "Fastest",
"fundingAmount": "Funding amount",
+ "createFund": "Create a fund",
+ "withdraw": "Withdraw",
"fast": "Fast",
"ecashDesc": "Ecash tokens are funded with bitcoin. They may be redeemed in exchange for a lightning payment equal to their value or loaded back into a coinos account at any time. They can also be transferred to other ecash wallets either wholly or partially. You might use them to give bitcoin to someone who doesn't have a wallet by texting the token to them or sharing a link to it. Learn more at",
"ecashToken": "Ecash token",
@@ -376,8 +379,8 @@
"slow": "Slow",
"create": "Create",
"fund": "Fund",
- "addFunds": "Add Funds",
- "takeFunds": "Take Funds",
+ "addFunds": "Add funds",
+ "takeFunds": "Take funds",
"show": "Show",
"showText": "Show text",
"showQr": "Show QR",
@@ -386,7 +389,7 @@
"qr": "QR",
"copy": "Copy",
"amountPrompt": "Prompt for amount",
- "shareLink": "Share link",
+ "shareLink": "Share",
"copyLink": "Copy link",
"copyText": "Copy text",
"openLink": "Open wallet",
diff --git a/src/routes/(app)/fund/[id]/+page.server.ts b/src/routes/(app)/fund/[id]/+page.server.ts
index 668ea3af..9ea33627 100644
--- a/src/routes/(app)/fund/[id]/+page.server.ts
+++ b/src/routes/(app)/fund/[id]/+page.server.ts
@@ -2,9 +2,12 @@ import getRates from "$lib/rates";
import { auth, get, post } from "$lib/utils";
import { redirect } from "@sveltejs/kit";
-export const load = async ({ params: { id } }) => {
+export const load = async ({ depends, params: { id }, parent }) => {
+ depends("app:payments");
const data = await get(`/fund/${id}`);
- data.rate = await getRates();
+ const rates = await getRates();
+ const { user } = await parent();
+ data.rate = rates[user?.currency || "USD"];
return data;
};
diff --git a/src/routes/(app)/fund/[id]/+page.svelte b/src/routes/(app)/fund/[id]/+page.svelte
index 23dce871..f99b17d7 100644
--- a/src/routes/(app)/fund/[id]/+page.svelte
+++ b/src/routes/(app)/fund/[id]/+page.svelte
@@ -1,28 +1,33 @@
<script>
+ import Amount from "$comp/Amount.svelte";
+ import Payments from "$comp/Payments.svelte";
import { format } from "date-fns";
import { enhance } from "$app/forms";
import { page } from "$app/stores";
import Avatar from "$comp/Avatar.svelte";
- import Icon from "$comp/Icon.svelte";
import { toast } from "@zerodevx/svelte-toast";
import { t } from "$lib/translations";
- import { types, copy, f, s, sat, sats } from "$lib/utils";
+ import { types, copy, f, s, sat, sats, loc } from "$lib/utils";
import { loginRedirect } from "$lib/store";
+ import { bech32 } from "@scure/base";
+ const encoder = new TextEncoder();
let { data } = $props();
- let { amount, src, payments, rate, user } = data;
+ let { amount, payments, rate, user } = data;
let { id } = $page.params;
-
- toast.pop(0);
+ let locale = loc(user);
let currency = user ? user.currency : "CAD";
let amountFiat = $derived(parseFloat(((amount * rate) / sats).toFixed(2)));
- $loginRedirect = $page.url.pathname;
+ $effect(() => ($loginRedirect = $page.url.pathname));
let show = $state();
let toggle = () => (show = !show);
+ let href = $derived(encodeURIComponent($page.url.href));
+ let withdrawUrl = encoder.encode(`${$page.url.origin}/api/fund/${id}/withdraw`);
+ let lnurlw = bech32.encode("lnurl", bech32.toWords(withdrawUrl), 20000);
</script>
<div class="container px-4 max-w-4xl mx-auto mt-10 space-y-5">
@@ -30,117 +35,55 @@
<div
class="md:shadow-xl rounded-3xl md:px-10 pt-5 pb-10 space-y-5 w-full md:mx-5"
>
- <div class="relative flex justify-center gap-2">
- <!-- <p class="absolute right-0 top-1 underline"><Icon icon="settings" /></p> -->
- {#if id.split("-").length === 4}
- <h1 class="text-2xl md:text-3xl font-semibold my-auto">Card {id}</h1>
- {:else}
- <img src="/images/moneypot.jpg" class="w-24" alt="Fund" />
- <div class="my-auto">
- <h1 class="text-2xl font-semibold my-auto">
- {$t("payments.fund")}
- </h1>
- <h1 class="text-secondary my-auto">{id}</h1>
- </div>
- {/if}
- </div>
{#if show}
- <img {src} class="mx-auto" alt={id} />
+ <div class="max-w-[360px] mx-auto">
+ <img
+ src={`/qr/${href}/raw`}
+ class="z-10 border-4 border-white w-"
+ alt="URL"
+ />
+ </div>
{/if}
<div class="flex justify-center gap-4">
- <div class="my-auto">
- <h2 class="text-2xl md:text-3xl font-semibold">
- {f(amountFiat, currency)}
- </h2>
- <h3 class="text-secondary md:text-lg mt-1">
- {sat(amount)}
- </h3>
- </div>
+ <Amount {amount} {currency} {rate} {locale} />
</div>
<div class="flex gap-2" data-sveltekit-prefetch="off">
<div class="grow">
<a href={`/send/fund/${id}`}>
- <button
- class="rounded-full border py-3 px-2 font-bold hover:opacity-80 flex gap-1 w-full justify-center"
- >
- <Icon icon="plus" />
+ <button class="btn">
+ <iconify-icon icon="ph:plus-bold" width="32"></iconify-icon>
{$t("payments.addFunds")}
</button>
</a>
</div>
<div class="grow">
<a href={`/fund/${id}/withdraw`}>
- <button
- class="rounded-full border py-3 px-2 font-bold hover:opacity-80 flex gap-1 w-full justify-center"
- >
- <Icon icon="minus" style="rotate-180" />
+ <button class="btn">
+ <iconify-icon
+ icon="ph:hand-coins-bold"
+ width="32"
+ flip="horizontal"
+ ></iconify-icon>
{$t("payments.takeFunds")}
</button>
</a>
</div>
</div>
<div class="flex gap-2" data-sveltekit-prefetch="off">
- <button
- class="rounded-full border py-3 px-2 font-bold hover:opacity-80 flex gap-1 w-full justify-center"
- onclick={toggle}
- >
- <Icon icon="qr" style="invert" />
- {show ? $t("payments.hide") : $t("payments.show")} QR
- </button>
- <button
- class="rounded-full border py-3 px-2 font-bold hover:opacity-80 flex gap-1 w-full justify-center"
- onclick={() => copy($page.url.host + $page.url.pathname)}
- >
- <Icon icon="copy" style="w-6" />
- {$t("payments.copyLink")}
- </button>
- </div>
- <div class="text-base">
- {#each payments as p}
- <a href={`/payment/${p.id}`}>
- <div class="grid grid-cols-3 border-b h-24 hover:bg-gray-100 px-4">
- <div class="whitespace-nowrap my-auto">
- <div class="font-bold" class:text-red-800={p.amount > 0}>
- {f(Math.abs(p.amount) * (p.rate / sats), p.currency)}
- {#if p.tip}
- <span class="text-sm text-secondary">
- +{Math.round((p.tip / Math.abs(p.amount)) * 100)}%
- </span>
- {/if}
- </div>
-
- <div class="text-secondary">
- {sat(Math.abs(p.amount) + (p.tip || 0))}
- </div>
- </div>
-
- <div class="flex my-auto">
- <div class="flex">
- <div class="my-auto">
- <Avatar user={p.user} size={16} disabled={true} />
- </div>
- <div class="my-auto ml-1 text-secondary">
- {p.user.username}
- </div>
- </div>
- </div>
-
- <div class="text-secondary text-right text-sm my-auto">
- <div>
- {format(new Date(p.created), "h:mm aaa")}
- </div>
- <div>
- {format(new Date(p.created), "MMM d")}
- </div>
- </div>
- </div>
- </a>
- {:else}
- <p class="text-secondary text-lg text-center">
- {$t("payments.empty")}
- </p>
- {/each}
+ <a href={`/qr/${href}`} class="btn !w-auto grow">
+ <iconify-icon icon="ph:link-bold" width="32"></iconify-icon>
+ <div class="my-auto">{$t("payments.shareLink")}</div>
+ </a>
+ <a href={`/qr/${lnurlw}`} class="btn !w-auto grow">
+ <iconify-icon
+ icon="ph:lightning-fill"
+ width="24"
+ class="text-yellow-300"
+ ></iconify-icon>
+ {$t("payments.lnurlw")}
+ </a>
</div>
+ <Payments {payments} fund={true} />
</div>
</div>
</div>
diff --git a/src/routes/(app)/fund/[id]/withdraw/+page.server.ts b/src/routes/(app)/fund/[id]/withdraw/+page.server.ts
index 1e477918..696c52ff 100644
--- a/src/routes/(app)/fund/[id]/withdraw/+page.server.ts
+++ b/src/routes/(app)/fund/[id]/withdraw/+page.server.ts
@@ -1,9 +1,15 @@
+import getRates from "$lib/rates";
+import { auth, fd, get, post } from "$lib/utils";
import { fail, redirect } from "@sveltejs/kit";
-import { fd, auth, post } from "$lib/utils";
-export async function load({ parent }) {
+export async function load({ params, parent }) {
const { user } = await parent();
if (!user) redirect(307, "/register");
+ const rates = await getRates();
+ const rate = rates[user.currency];
+ const fund = await get(`/fund/${params.id}`);
+ const { amount: balance } = fund;
+ return { ...params, balance, rate };
}
export const actions = {
@@ -11,8 +17,9 @@ export const actions = {
const body = await fd(request);
try {
await post("/take", body, auth(cookies));
- } catch (e: any) {
- return fail(400, { message: e.message });
+ } catch (e) {
+ const { message } = e as Error;
+ return fail(400, { message });
}
redirect(307, `/fund/${body.id}`);
diff --git a/src/routes/(app)/fund/[id]/withdraw/+page.svelte b/src/routes/(app)/fund/[id]/withdraw/+page.svelte
index 89654d23..f6f12b1a 100644
--- a/src/routes/(app)/fund/[id]/withdraw/+page.svelte
+++ b/src/routes/(app)/fund/[id]/withdraw/+page.svelte
@@ -1,40 +1,35 @@
<script>
- import { run } from 'svelte/legacy';
-
+ import handler from "$lib/handler";
import { t } from "$lib/translations";
import { enhance } from "$app/forms";
- import Icon from "$comp/Icon.svelte";
import Numpad from "$comp/Numpad.svelte";
import Spinner from "$comp/Spinner.svelte";
import { page } from "$app/stores";
- import { back } from "$lib/utils";
+ import { s } from "$lib/utils";
import { pin } from "$lib/store";
let { data, form } = $props();
- let { rates } = data;
- let { amount, id } = $state($page.params);
- let { currency } = $state(data.user);
+ let { currency } = $derived(data.user);
let loading = $state();
+ let { id, balance, rate } = $derived(data);
+ let { amount } = $state(data);
+ let fiat = $state();
- let submit = () => (loading = true);
- let update = () => {
+ let submit = $state();
+ let submitting = $state();
+ let toggle = () => (submitting = !submitting);
+ $effect(() => {
if (form?.message.includes("pin")) $pin = undefined;
loading = false;
- };
-
- run(() => {
- update(form);
});
- let rate;
- run(() => {
- rate = rates[currency];
- });
-</script>
-<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" onclick={back}>
- <Icon icon="arrow-left" style="w-10" />
-</button>
+ let setMax = (e) => {
+ e.preventDefault();
+ fiat = false;
+ amount = balance;
+ };
+</script>
{#if form?.message}
<div class="text-red-600 text-center">
@@ -43,22 +38,32 @@
{/if}
<div class="container px-4 mt-20 max-w-xl mx-auto">
- <Numpad bind:amount bind:currency bind:rate />
+ <Numpad bind:amount {currency} {fiat} {rate} {submit} />
- <form method="POST" use:enhance onsubmit={submit}>
+ <form use:enhance={handler(toggle)} method="POST" class="space-y-5">
<input name="id" value={id} type="hidden" />
<input name="amount" value={amount} type="hidden" />
<input name="pin" value={$pin} type="hidden" />
- <div class="flex w-full">
+ <div class="flex gap-2">
+ <button
+ type="button"
+ class="btn !w-auto grow"
+ onclick={setMax}
+ disabled={submitting}
+ onkeydown={setMax}>Max ⚡️{s(balance)}</button
+ >
+
<button
+ use:focus
+ bind:this={submit}
type="submit"
- class="opacity-100 hover:opacity-80'} rounded-2xl border py-3 font-bold mx-auto mt-2 bg-black text-white px-4 w-24"
+ class="btn btn-accent !w-auto grow"
>
- {#if loading}
+ {#if submitting}
<Spinner />
{:else}
- {$t("payments.send")}
+ <div class="my-auto">{$t("payments.next")}</div>
{/if}
</button>
</div>
diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.ts b/src/routes/(app)/ln/[lnurl]/+page.server.ts
index 681137ad..53445461 100644
--- a/src/routes/(app)/ln/[lnurl]/+page.server.ts
+++ b/src/routes/(app)/ln/[lnurl]/+page.server.ts
@@ -7,10 +7,12 @@ export async function load({ params, parent }) {
if (!user) redirect(307, "/register");
const rates = await getRates();
+ console.log("YO");
let data;
try {
const { lnurl } = params;
data = await get(`/decode?text=${lnurl}`);
+ console.log("DATA", data);
} catch (e) {
const { message } = e as Error;
error(500, message);
@@ -76,8 +78,11 @@ export const actions = {
auth(cookies),
);
- const c = callback.includes("?") ? "&" : "?";
- await fetch(`${callback}${c}k1=${k1}&pr=${pr}`).then((r) => r.json());
+ const url = new URL(callback);
+ url.searchParams.append("k1", k1);
+ url.searchParams.append("pr", pr);
+
+ await fetch(url.toString());
redirect(307, "/payments");
},
diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte
index 279b87e7..63b8ccca 100644
--- a/src/routes/(app)/payment/[id]/+page.svelte
+++ b/src/routes/(app)/payment/[id]/+page.svelte
@@ -112,7 +112,11 @@
<a href={`/fund/${p.memo}`}>
<div class="flex">
<div class="my-auto">
- <img src="/images/moneypot.png" class="w-12" alt="Pot" />
+ <iconify-icon
+ icon="ph:lightning-fill"
+ width="24"
+ class="text-yellow-300"
+ ></iconify-icon>
</div>
<div class="my-auto ml-1">{p.memo}</div>
</div>
diff --git a/src/routes/(app)/payments/[...page]/+page.svelte b/src/routes/(app)/payments/[...page]/+page.svelte
index d53c13c0..09bdf4c7 100644
--- a/src/routes/(app)/payments/[...page]/+page.svelte
+++ b/src/routes/(app)/payments/[...page]/+page.svelte
@@ -1,6 +1,6 @@
<script>
- import { run } from "svelte/legacy";
import Avatar from "$comp/Avatar.svelte";
+ import Payments from "$comp/Payments.svelte";
import { onMount } from "svelte";
import { format } from "date-fns";
import { newPayment } from "$lib/store";
@@ -193,113 +193,17 @@
</div>
</a>
{:else if isEllipsis(i)}
- <div class="join-item btn !w-auto !btn-sm btn-disabled !bg-base-200">...</div>
+ <div
+ class="join-item btn !w-auto !btn-sm btn-disabled !bg-base-200"
+ >
+ ...
+ </div>
{/if}
{/each}
{/if}
</div>
- <div class="text-base">
- {#each payments as p, i}
- <div
- class="grid grid-cols-12 border-b border-base-200 hover:bg-base-200 px-1 py-2 lg:p-4 cursor-pointer"
- class:border-b-0={i === payments.length - 1}
- class:text-error={p.amount < 0}
- onclick={() => goto(`/payment/${p.id}`)}
- >
- <div class="whitespace-nowrap my-auto col-span-3">
- <div class="font-bold flex items-center">
- <div class="flex items-center">
- <iconify-icon
- icon="ph:lightning-fill"
- width="24"
- class="text-yellow-300"
- ></iconify-icon>
- <div>{s(Math.abs(p.amount), userLocale)}</div>
- </div>
- </div>
-
- <div class="text-secondary flex items-center text-base">
- {f(Math.abs(p.amount) * (p.rate / sats), p.currency, userLocale)}
-
- {#if p.tip}
- <span class="text-sm text-secondary">
- &nbsp;+{Math.round((p.tip / Math.abs(p.amount)) * 100)}%
- </span>
- {/if}
- </div>
- </div>
-
- <div
- class="flex my-auto col-span-6 truncate text-ellipsis overflow-hidden mx-auto"
- >
- {#if p.type === types.pot}
- <a href={`/pot/${p.memo}`}>
- <div class="text-secondary flex">
- <div class="my-auto mr-1">
- <img src="/images/moneypot.png" class="w-12" alt="Pot" />
- </div>
-
- <div class="my-auto">Pot</div>
- </div>
- </a>
- {:else if p.with}
- <div class="flex">
- <div class="my-auto">
- <Avatar user={p.with} size={12} disabled={true} />
- </div>
- <div class="my-auto ml-1 text-secondary">
- {p.with.username}
- </div>
- </div>
- {:else}
- <div class="text-secondary flex items-center gap-1">
- {#if p.type === types.lightning}
- <iconify-icon
- icon="ph:lightning-fill"
- class="text-yellow-300 text-3xl"
- ></iconify-icon>
- {:else if p.type === types.ecash}
- <img src="/images/cash.png" class="w-12" />
- {:else if p.type === types.reconcile}
- <iconify-icon icon="ph:scales-bold" width="32"></iconify-icon>
- {:else if p.type === types.bitcoin}
- <iconify-icon icon="logos:bitcoin" class="text-3xl"
- ></iconify-icon>
- {:else if p.type === types.liquid}
- <div class="my-auto">
- <img
- src="/images/liquid.svg"
- class="w-10 border-4 border-transparent"
- alt="Liquid"
- />
- </div>
- {/if}
-
- <div class="my-auto">
- {p.amount > 0
- ? p.confirmed
- ? "Received"
- : "Pending"
- : "Sent"}
- </div>
- </div>
- {/if}
- </div>
-
- <div class="text-secondary text-right text-sm my-auto col-span-3">
- <div>
- {format(new Date(p.created), "h:mm aaa", { locale })}
- </div>
- <div>
- {format(new Date(p.created), "MMM d, yy", { locale })}
- </div>
- </div>
- </div>
- {:else}
- <p class="text-secondary text-lg text-center">{$t("payments.empty")}</p>
- {/each}
- </div>
+ <Payments {payments} />
<div class="grid grid-cols-3 w-full text-center text-lg">
{#each Object.keys(incoming) as c}
diff --git a/src/routes/(app)/qr/[text]/+page.svelte b/src/routes/(app)/qr/[text]/+page.svelte
index 46420743..ff5e8fa8 100644
--- a/src/routes/(app)/qr/[text]/+page.svelte
+++ b/src/routes/(app)/qr/[text]/+page.svelte
@@ -3,7 +3,7 @@
import Qr from "$comp/Qr.svelte";
import { back, copy } from "$lib/utils";
let { data } = $props();
- let { text } = data;
+ let { text } = $derived(data);
</script>
<div class="container px-4 max-w-4xl mx-auto space-y-5">
diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte
index ebc705b5..261a1784 100644
--- a/src/routes/(app)/send/[...text]/+page.svelte
+++ b/src/routes/(app)/send/[...text]/+page.svelte
@@ -94,6 +94,14 @@
<div class="my-auto">{$t("payments.createEcash")}</div>
</button>
</a>
+
+ <a href="/send/fund" class="block">
+ <button type="button" class="btn">
+ <iconify-icon icon="ph:lightning-fill" class="text-yellow-300 text-2xl"
+ ></iconify-icon>
+ <div class="my-auto">{$t("payments.createFund")}</div>
+ </button>
+ </a>
</form>
{#if contacts.length}
diff --git a/src/routes/(app)/send/fund/[...name]/[...amount]/+page.server.ts b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.server.ts
new file mode 100644
index 00000000..428d8d78
--- /dev/null
+++ b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.server.ts
@@ -0,0 +1,28 @@
+import getRates from "$lib/rates";
+import { auth, fd, post } from "$lib/utils";
+import { fail, redirect } from "@sveltejs/kit";
+
+export async function load({ parent, params }) {
+ let { name, amount } = params;
+ if (!name) name = crypto.randomUUID();
+ const { user } = await parent();
+ const rates = await getRates();
+ const rate = rates[user.currency];
+ return { name, amount, rate };
+}
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ const body = await fd(request);
+ let p;
+
+ try {
+ p = await post("/payments", body, auth(cookies));
+ } catch (e) {
+ const { message } = e as Error;
+ return fail(400, { message });
+ }
+
+ if (p) redirect(307, `/fund/${body.fund}`);
+ },
+};
diff --git a/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte
new file mode 100644
index 00000000..314be3ef
--- /dev/null
+++ b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte
@@ -0,0 +1,70 @@
+<script>
+ import { t } from "$lib/translations";
+ import { enhance } from "$app/forms";
+ import Numpad from "$comp/Numpad.svelte";
+ import Spinner from "$comp/Spinner.svelte";
+ import { page } from "$app/stores";
+ import { pin } from "$lib/store";
+ import handler from "$lib/handler";
+ import { s } from "$lib/utils";
+
+ let { data, form } = $props();
+ let { amount } = $state(data);
+ let { balance, currency } = data.user;
+ let { name, rate } = $derived(data);
+ let loading = $state();
+ let fiat = $state();
+
+ let submit = $state();
+ let submitting = $state();
+ let toggle = () => (submitting = !submitting);
+ $effect(() => {
+ if (form?.message?.includes("pin")) $pin = undefined;
+ loading = false;
+ });
+
+ let setMax = (e) => {
+ e.preventDefault();
+ fiat = false;
+ amount = balance;
+ };
+</script>
+
+{#if form?.message}
+ <div class="text-red-600 text-center">
+ {form.message}
+ </div>
+{/if}
+
+<div class="container px-4 mt-20 max-w-xl mx-auto">
+ <Numpad bind:amount {currency} {rate} {fiat} {submit} />
+
+ <form use:enhance={handler(toggle)} method="POST" class="space-y-5">
+ <input name="fund" value={name} type="hidden" />
+ <input name="amount" value={amount} type="hidden" />
+ <input name="pin" value={$pin} type="hidden" />
+
+ <div class="flex gap-2">
+ <button
+ type="button"
+ class="btn !w-auto grow"
+ onclick={setMax}
+ disabled={submitting}
+ onkeydown={setMax}>Max ⚡️{s(balance)}</button
+ >
+
+ <button
+ use:focus
+ bind:this={submit}
+ type="submit"
+ class="btn btn-accent !w-auto grow"
+ >
+ {#if submitting}
+ <Spinner />
+ {:else}
+ <div class="my-auto">{$t("payments.next")}</div>
+ {/if}
+ </button>
+ </div>
+ </form>
+</div>
diff --git a/src/routes/(app)/send/fund/[name]/[...amount]/+page.server.ts b/src/routes/(app)/send/fund/[name]/[...amount]/+page.server.ts
deleted file mode 100644
index ed39f96b..00000000
--- a/src/routes/(app)/send/fund/[name]/[...amount]/+page.server.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { fail, redirect } from "@sveltejs/kit";
-import { auth, fd, post } from "$lib/utils";
-
-export const actions = {
- default: async ({ cookies, request }) => {
- const body = await fd(request);
- let p;
-
- try {
- p = await post("/payments", body, auth(cookies));
- } catch (e: any) {
- return fail(400, { message: e.message });
- }
-
- if (p) redirect(307, `/fund/${body.name}`);
- },
-};
diff --git a/src/routes/(app)/send/fund/[name]/[...amount]/+page.svelte b/src/routes/(app)/send/fund/[name]/[...amount]/+page.svelte
deleted file mode 100644
index d1c3b204..00000000
--- a/src/routes/(app)/send/fund/[name]/[...amount]/+page.svelte
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
- import { run } from 'svelte/legacy';
-
- import { t } from "$lib/translations";
- import { enhance } from "$app/forms";
- import Icon from "$comp/Icon.svelte";
- import Numpad from "$comp/Numpad.svelte";
- import Spinner from "$comp/Spinner.svelte";
- import { page } from "$app/stores";
- import { back } from "$lib/utils";
- import { pin } from "$lib/store";
-
- let { data, form } = $props();
-
- let { amount, name } = $state($page.params);
- let { rates } = data;
- let { currency } = data.user;
- let loading = $state();
-
-
- let submit = () => (loading = true);
- let update = () => {
- if (form?.message?.includes("pin")) $pin = undefined;
- loading = false;
- };
- let rate;
- run(() => {
- rate = rates[currency];
- });
- run(() => {
- update(form);
- });
-</script>
-
-<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" onclick={back}>
- <Icon icon="arrow-left" style="w-10" />
-</button>
-
-{#if form?.message}
- <div class="text-red-600 text-center">
- {form.message}
- </div>
-{/if}
-
-<div class="container px-4 mt-20 max-w-xl mx-auto">
- <Numpad bind:amount {currency} bind:rate />
-
- <form method="POST" use:enhance onsubmit={submit}>
- <input name="name" value={name} type="hidden" />
- <input name="amount" value={amount} type="hidden" />
- <input name="pin" value={$pin} type="hidden" />
-
- <div class="flex w-full">
- <button
- type="submit"
- class="opacity-100 hover:opacity-80'} rounded-2xl border py-3 font-bold mx-auto mt-2 bg-black text-white px-4 w-24"
- >
- {#if loading}
- <Spinner />
- {:else}
- {$t("payments.send")}
- {/if}
- </button>
- </div>
- </form>
-</div>