diff options
| author | John Barratt <[email protected]> | 2023-03-07 10:29:49 -0800 |
|---|---|---|
| committer | John Barratt <[email protected]> | 2023-03-07 10:29:49 -0800 |
| commit | 8ef9568cd1bd822495ce005d4e4cfaa6cb0d92ce (patch) | |
| tree | 4ea5530cc2b7e5d970dc8a84cacfecfe2cce46b7 /src/routes | |
| parent | c97fb43abba375920fff361ec5fa4eb64a5a0d3f (diff) | |
| parent | 0f44e3283c25c9783a52cf20957661da49d2f90b (diff) | |
| download | coinos-ui-notifications.tar.gz coinos-ui-notifications.zip | |
Merge branch 'master' of https://github.com/coinos/coinos-ui into notificationsnotifications
Diffstat (limited to 'src/routes')
49 files changed, 1057 insertions, 487 deletions
diff --git a/src/routes/(app)/+error.svelte b/src/routes/(app)/+error.svelte new file mode 100644 index 00000000..36726e26 --- /dev/null +++ b/src/routes/(app)/+error.svelte @@ -0,0 +1,38 @@ +<script> + import { back } from '$lib/utils'; + import { Icon } from '$comp'; + import { page } from '$app/stores'; + + export let data; + let { user } = data; + + let link = user ? `/${user.username}` : '/'; +</script> + +<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5"> + <div class="w-full flex"> + <a href="/" class="mx-auto"> + <Icon icon="logo" /> + </a> + </div> + + <div class="flex justify-center items-center"> + <div class="shadow-xl rounded-3xl px-10 pt-5 pb-10 space-y-5 w-full mx-5"> + <div class="relative"> + <h1 class="text-2xl md:text-3xl font-semibold text-center">Error</h1> + </div> + <p class="text-secondary text-center">Something went wrong</p> + <p class="text-secondary text-center">{$page.error.message}</p> + + <div class="flex justify-center py-24"> + <a href={link}> + <button + class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 text-xl" + > + Back + </button> + </a> + </div> + </div> + </div> +</div> diff --git a/src/routes/(app)/+layout.server.js b/src/routes/(app)/+layout.server.js index ee4ce550..3442081b 100644 --- a/src/routes/(app)/+layout.server.js +++ b/src/routes/(app)/+layout.server.js @@ -19,7 +19,7 @@ export async function load({ cookies, request, url, params }) { throw redirect(307, `/migrate`); } - if (user && ['/', '/login', '/register'].includes(pathname) && request.method === 'GET') { + if (user && ['/login', '/register'].includes(pathname) && request.method === 'GET') { throw redirect(307, `/${user.username}`); } diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 2a122209..45469bdb 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -58,7 +58,7 @@ onDestroy(() => browser && clearTimeout(timer)); </script> -{#if $passwordPrompt} +{#if browser && $passwordPrompt} <Password {user} /> {/if} diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index dfa25b78..133930b1 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -4,7 +4,6 @@ import { Icon } from '$comp'; import { t } from '$lib/translations'; import { sign, send } from '$lib/nostr'; - import { v4 } from 'uuid'; export let data; let events, user, subject, src, text; @@ -111,7 +110,7 @@ {/if} <div class="w-full flex"> - <a href={`/${subject.pubkey}/messages`} class="mx-auto"> + <a href={`/${subject.username}/messages`} class="mx-auto"> <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> <div class="mx-auto flex"> <Icon icon="support" style="mr-2 my-auto" /> diff --git a/src/routes/(app)/[username]/(profile)/+page.server.js b/src/routes/(app)/[username]/(profile)/+page.server.js index c68cfecc..d6cf3984 100644 --- a/src/routes/(app)/[username]/(profile)/+page.server.js +++ b/src/routes/(app)/[username]/(profile)/+page.server.js @@ -1,22 +1,35 @@ import { auth, get } from '$lib/utils'; export async function load({ cookies, params, parent, url }) { - let { subject } = await parent(); + let { user, subject } = await parent(); let { pubkey } = subject; + let { since = 0 } = params; - let events = []; + let messages, invoices, sent, received; + messages = invoices = sent = received = []; + + if (user) { + try { + messages = await get(`/${user.pubkey}/${since}/messages`); + messages = messages.sort((a, b) => b.created_at - a.created_at); + } catch (e) { + console.log(`failed to fetch nostr messages`, e); + } + + ({ invoices, sent, received } = await get('/requests', auth(cookies))); + } + + let notes = []; try { - events = await get(`/${pubkey}/notes`); + notes = await get(`/${pubkey}/notes`); } catch (e) { - console.log(`failed to fetch nostr events for ${pubkey}`, e); + console.log(`failed to fetch nostr notes for ${pubkey}`, e); } - events.map((e) => { + notes.map((e) => { e.seen = e.created_at; }); - let { invoices, sent, received } = await get('/requests', auth(cookies)); - - return { invoices, events, sent, received }; + return { invoices, messages, notes, sent, received }; } diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index baab8ba1..168556e4 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -1,76 +1,197 @@ <script> - import { Avatar, Balance, Feed } from '$comp'; + import { onMount } from 'svelte'; + import { Icon, Avatar, Balance, Feed } from '$comp'; import { t } from '$lib/translations'; import { f, sat, sats } from '$lib/utils'; + import { sign, send, encrypt, decrypt } from '$lib/nostr'; + import { event as e, password } from '$lib/store'; + import { browser } from '$app/environment'; export let data; - let { events, invoices, sent, received, subject, user } = data; - console.log(invoices); + let { messages, notes, invoices, sent, received, subject, user } = data; + $: refresh(data); + let refresh = (d) => ({ messages, notes, invoices, sent, received, subject, user } = d); + + let keys = new Set(); + let latest = []; + let i = 0; + let ready; + + e.subscribe(async (event) => { + if (!(event && ready)) return; + if (event.recipient.id === user?.id && !~latest.findIndex((m) => m.id === event.id)) { + event.content = await decrypt({ event, user }); + + let i = latest.findIndex((m) => m.pubkey === event.pubkey); + let popped; + if (~i) popped = latest.splice(i, 1); + else popped = latest.pop(); + + latest.unshift(event); + latest = latest; + } + }); + + $: initialize($password); + let initialize = async (p) => { + ready = false; + if (!p) return; + while (latest.length < 3 && i < messages.length) { + let event = messages[i]; + let k = event.author.id === user.id ? event.recipient.pubkey : event.author.pubkey; + if (!keys.has(k)) { + keys.add(k); + event.content = await decrypt({ event, user }); + if (event.content) latest.push(event); + } + i++; + } + + latest = latest; + ready = true; + }; </script> -<div class="flex justify-center lg:justify-start"> +<div class="space-y-8"> {#if user?.id === subject.id} - <Balance {user} /> - {/if} -</div> + <div> + <div class="flex justify-center lg:justify-start"> + <Balance {user} /> + </div> + {#if !user.balance} + <div class="space-y-8 mt-8"> + <p class="text-secondary text-lg"> + Welcome! Your account balance is empty. You can start by making a deposit or by asking + someone to pay you. + </p> -<div> - {#each invoices as { amount, currency, rate, request: { requester, recipient }, hash }} - {@const requested = user.username === requester?.username} - <a href={`/${recipient.username}/invoice/${hash}` + (requested ? '/tip' : '')}> - <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> - <div class="flex gap-2"> - <div> - {#if requested} - <div class="flex"> - <Avatar user={recipient} size={20} /> - <div class="my-auto text-left"> - <p class="text-secondary">{$t('payments.invoiceFrom')}</p> - <p class="ml-1 text-lg break-words">{recipient.username}</p> + <div class="flex gap-4 justify-center w-full"> + <a href={`/${user.username}/receive`}> + <button + class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60 bg-black text-white" + > + <div class="mx-auto flex"> + <Icon icon="numpad" style="my-auto h-6 mr-2 invert" /> + <div class="my-auto mt-1">Request Payment</div> </div> + </button> + </a> + + {#if user.eligible} + <a href={`/buy`}> + <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> + <div class="mx-auto flex"> + <!-- <Icon icon="plus" style="my-auto h-6 mr-2" /> --> + + <img + src="/images/bitcoin.svg" + class="w-8 border-4 border-transparent mr-2" + alt="Bitcoin" + /> + <div class="my-auto mt-1">Buy Bitcoin</div> + </div> + </button> + </a> + {/if} + </div> + </div> + {/if} + </div> + {/if} + + {#if invoices.length} + <div> + <h1 class="text-secondary text-xl">Invoices</h1> + {#each invoices as { amount, currency, rate, request: { requester, recipient }, hash }} + {@const requested = user.username === requester?.username} + <a href={`/${recipient.username}/invoice/${hash}` + (requested ? '/tip' : '')}> + <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> + <div class="flex gap-2"> + <div> + {#if requested} + <div class="flex"> + <Avatar user={recipient} size={20} /> + <div class="my-auto text-left"> + <p class="text-secondary">{$t('payments.invoiceFrom')}</p> + <p class="ml-1 text-lg break-words">{recipient.username}</p> + </div> + </div> + {:else} + <div class="flex"> + <Avatar user={requester} size={20} /> + <div class="my-auto text-left"> + <p class="text-secondary">{$t('payments.awaiting')}</p> + <p class="ml-1 text-lg break-words">{requester.username}</p> + </div> + </div> + {/if} </div> - {:else} - <div class="flex"> - <Avatar user={requester} size={20} /> - <div class="my-auto text-left"> - <p class="text-secondary">{$t('payments.awaiting')}</p> - <p class="ml-1 text-lg break-words">{requester.username}</p> + <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> + <div class="font-bold"> + {f(amount * (rate / sats), currency)} </div> + + <div class="text-secondary">{sat(amount)}</div> </div> - {/if} - </div> - <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> - <div class="font-bold"> - {f(amount * (rate / sats), currency)} </div> - - <div class="text-secondary">{sat(amount)}</div> </div> - </div> - </div> - </a> - {/each} + </a> + {/each} + </div> + {/if} + {#if received.length} + <div> + <h1 class="text-secondary text-xl">Requests</h1> - {#each received as { id, invoice, memo, requester: r }} - <a href={`/${user.username}/receive/${id}`}> - <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> - <div class="flex"> - <div> + {#each received as { id, invoice, memo, requester: r }} + <a href={`/${user.username}/receive/${id}`}> + <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> <div class="flex"> - <Avatar user={r} size={20} /> - <div class="my-auto text-left"> - <p class="ml-1 text-lg break-words">{r.username}</p> - <p class="ml-1 text-secondary">{memo}</p> + <div> + <div class="flex"> + <Avatar user={r} size={20} /> + <div class="my-auto text-left"> + <p class="ml-1 text-lg break-words">{r.username}</p> + <p class="ml-1 text-secondary">{memo}</p> + </div> + </div> + </div> + <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> + <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full" + >Invoice</button + > </div> </div> </div> - <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> - <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full" - >Invoice</button - > + </a> + {/each} + </div> + {/if} + + {#if latest.length && user?.id === subject.id} + <div class="relative"> + {#each latest as { content, pubkey, author, recipient }} + {@const user = author.id === user.id ? recipient : author} + <a href={`/${user.username}/messages`}> + <div class="flex hover:bg-gray-100 p-4 rounded-2xl"> + <div class="my-auto"> + <Avatar {user} size={'20'} disabled={true} /> + </div> + <div class="my-auto truncate"> + <div class="my-auto ml-1 text-lg font-bold">{user.username}</div> + <div class="my-auto ml-1 text-secondary text-lg truncate"> + {content} + </div> + </div> </div> - </div> - </div> - </a> - {/each} + </a> + {/each} + </div> + {/if} </div> + +<style> + h1 { + @apply mb-2; + } +</style> diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.svelte b/src/routes/(app)/[username]/(profile)/messages/+page.svelte deleted file mode 100644 index db7b1ced..00000000 --- a/src/routes/(app)/[username]/(profile)/messages/+page.svelte +++ /dev/null @@ -1,123 +0,0 @@ -<script> - import { browser } from '$app/environment'; - import { t } from '$lib/translations'; - import { format, parseISO } from 'date-fns'; - import { scale } from 'svelte/transition'; - import { enhance } from '$app/forms'; - import { Icon } from '$comp'; - import { back, fail, focus } from '$lib/utils'; - import { sign, send, encrypt, decrypt } from '$lib/nostr'; - import { messages, password } from '$lib/store'; - import { tick, onMount } from 'svelte'; - import { calculateId } from 'nostr'; - - export let data; - - let { newMessages, subject, user } = data; - let input, pane; - - $: initialize($password); - let initialize = async (p) => { - if (!p) return; - $messages[user.pubkey] = $messages[user.pubkey] || {}; - $messages[user.pubkey][subject.pubkey] = $messages[user.pubkey][subject.pubkey] || {}; - - for (let m of newMessages) { - if (!$messages[user.pubkey][subject.pubkey][m.id]) { - m.message = await decrypt({ event: m, user }); - $messages[user.pubkey][subject.pubkey][m.id] = m; - tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); - } - } - tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); - }; - - $: events = - ($messages[user.pubkey] && - $messages[user.pubkey][subject.pubkey] && - Object.values($messages[user.pubkey][subject.pubkey]).sort( - (a, b) => a.created_at - b.created - )) || - []; - - let sent, submitting, message; - let submit = async () => { - submitting = true; - - let event = { - pubkey: user.pubkey, - created_at: Math.floor(Date.now() / 1000), - kind: 4, - tags: [['p', subject.pubkey]] - }; - - event.message = message; - event.id = await calculateId(event); - $messages[user.pubkey][subject.pubkey][event.id] = event; - $messages = $messages; - tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); - - try { - event.content = await encrypt({ message, recipient: subject.pubkey, user }); - await sign({ event, user }); - await send(event); - - sent = true; - } catch (e) { - fail('Failed to send message'); - delete $messages[user.pubkey][subject.pubkey][event.id]; - $messages = $messages; - } - - message = ''; - submitting = false; - }; -</script> - -<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - {$t('payments.sendMessage')} - </h1> - - <div class="max-h-[calc(50vh)] overflow-y-scroll p-4" bind:this={pane}> - {#each events as data} - {@const ours = data.pubkey === user.pubkey} - {@const theirs = !ours} - <div class="rounded-2xl p-4 max-w-[300px] mb-1 text-md" class:ours class:theirs> - {#if data.message} - {data.message} - {:else} - - - {/if} - </div> - <div class="text-sm text-gray-400 mb-6" class:text-right={ours}> - {format(new Date(data.created_at * 1000), 'MMM d')}, - {format(new Date(data.created_at * 1000), 'h:mm aa')} - </div> - {/each} - </div> - - <form method="POST" class="space-y-5" on:submit|preventDefault={submit}> - <input type="hidden" name="requester_id" value={user.id} /> - <input type="hidden" name="recipient" value={subject.username} /> - - <input - use:focus - name="message" - placeholder={$t('payments.sendMessage')} - class="w-full p-4 border rounded-xl" - bind:value={message} - bind:this={input} - /> - </form> -</div> - -<style> - .ours { - @apply bg-gray-100 rounded-br-none ml-auto; - } - - .theirs { - @apply bg-gray-200 rounded-bl-none; - } -</style> diff --git a/src/routes/(app)/[username]/(profile)/request/+page.server.js b/src/routes/(app)/[username]/(profile)/request/+page.server.js index 2c1d6bde..e7ad6a8f 100644 --- a/src/routes/(app)/[username]/(profile)/request/+page.server.js +++ b/src/routes/(app)/[username]/(profile)/request/+page.server.js @@ -1,15 +1,15 @@ import { redirect } from '@sveltejs/kit'; -import { auth, post } from '$lib/utils'; +import { fd, auth, post } from '$lib/utils'; export const actions = { create: async ({ cookies, params, request, url }) => { - let form = Object.fromEntries(await request.formData()); + let form = await fd(request); let { id, requester } = await post('/requests', form, auth(cookies)); throw redirect(307, `/${params.username}/request/${id}`); }, delete: async ({ cookies, request }) => { - let form = Object.fromEntries(await request.formData()); + let form = await fd(request); let r = await post('/requests/delete', form, auth(cookies)); } }; diff --git a/src/routes/(app)/[username]/dashboard/+page.svelte b/src/routes/(app)/[username]/dashboard/+page.svelte index 499034e6..d8761ea9 100644 --- a/src/routes/(app)/[username]/dashboard/+page.svelte +++ b/src/routes/(app)/[username]/dashboard/+page.svelte @@ -7,7 +7,6 @@ import { goto } from '$app/navigation'; import { t } from '$lib/translations'; import { events } from '$lib/store'; - import { v4 } from 'uuid'; export let data; $: ({ invoices, requests } = data); diff --git a/src/routes/(app)/[username]/invoice/[hash]/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/+page.svelte index ad9721e7..fff2b22c 100644 --- a/src/routes/(app)/[username]/invoice/[hash]/+page.svelte +++ b/src/routes/(app)/[username]/invoice/[hash]/+page.svelte @@ -1,12 +1,13 @@ <script> + import { enhance } from '$app/forms'; import { send } from '$lib/socket'; - import { back, copy, f, get, sat, reverseFormat, s, sats } from '$lib/utils'; + import { post, back, copy, f, get, types, sat, reverseFormat, s, sats } from '$lib/utils'; import { tick, onMount, onDestroy } from 'svelte'; import { browser } from '$app/environment'; import { last } from '$lib/store'; import { Avatar, Icon, Heart, Image } from '$comp'; import { t } from '$lib/translations'; - import screenfull from 'screenfull'; + import { goto, invalidate } from '$app/navigation'; export let data; @@ -21,18 +22,19 @@ prompt, text, tip, + request_id, user: { username, currency } } = invoice; let showQr = !amount; - let src = sm; + $: src = sm; let qr; let tipPercent = 0; let refresh = (data) => { - ({ invoice, id } = data); + ({ invoice, id, user, sm, lg } = data); ({ amount, hash, @@ -64,31 +66,54 @@ $: link = type === 'bitcoin' ? text : `lightning:${text}`; $: txt = type === 'bitcoin' ? hash : text; - let full; - let toggle = () => { - screenfull.toggle(qr); - full = !full; - if (full) src = lg; - else src = sm; + let toggleType = async () => { + invoice.type = invoice.type === types.lightning ? types.bitcoin : types.lightning; + ({ hash } = await post(`/${username}/invoice`, { + invoice, + user: { username, currency } + })); + + goto(`./${hash}`, { invalidateAll: true }); }; </script> <div class="container mx-auto max-w-lg px-4 space-y-5"> - <div class="relative flex"> - <div class="flex mx-auto w-[360px] h-[360px]"> + <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> + <button + class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full" + class:bg-black={type === types.lightning} + class:text-white={type === types.lightning} + on:click={toggleType} + > + ⚡️ Lightning + </button> + + <button + class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full flex justify-center " + class:bg-black={type === types.bitcoin} + class:text-white={type === types.bitcoin} + on:click={toggleType} + > <img - {src} - class:p-4={full} - class="w-[300px] h-[300px] mx-auto z-10 mt-[20px]" - bind:this={qr} - on:click={toggle} - on:keydown={toggle} - alt={txt} + src="/images/bitcoin.svg" + class="my-auto w-8 border-4 border-transparent" + alt="Bitcoin" /> - </div> - <div - class="absolute m-auto left-0 right-0 w-[340px] h-[340px] rounded-full bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] z-0" - /> + <div class="my-auto">Bitcoin</div> + </button> + </div> + + <div> + <a href={invoice.type === 'bitcoin' ? invoice.text : 'lightning:' + invoice.text}> + <div class="relative flex"> + <div class="flex mx-auto w-[360px] h-[360px]"> + <img {src} class="w-[300px] h-[300px] mx-auto z-10 mt-[20px]" bind:this={qr} alt={txt} /> + </div> + <div + class="absolute m-auto left-0 right-0 w-[340px] h-[340px] rounded-full bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] z-0" + /> + </div> + </a> </div> {#if amount > 0} @@ -115,9 +140,9 @@ {/if} <div class="w-full flex justify-center"> - <button class="flex rounded-full border py-2 px-5 hover:opacity-80" on:click={() => copy(txt)}> + <button class="flex rounded-full border py-3 px-5 hover:opacity-80" on:click={() => copy(txt)}> <Icon icon="copy" style="mr-1" /> - <div class="text-secondary">{txt.substr(0, 10)}..{txt.substr(-6)}</div></button + <div class="text-secondary">{txt.substr(0, 10)}...{txt.substr(-10)}</div></button > </div> </div> diff --git a/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte index 432b137a..3a281728 100644 --- a/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte +++ b/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte @@ -4,7 +4,7 @@ import { toast } from '@zerodevx/svelte-toast'; import { browser } from '$app/environment'; import { t } from '$lib/translations'; - import { f, s, sat, sats } from '$lib/utils'; + import { fiat, f, s, sat, sats } from '$lib/utils'; import { goto } from '$app/navigation'; export let data; @@ -12,10 +12,6 @@ $: data && ({ amount, currency, rate, received, pending, tip, user, id } = data.invoice); toast.pop(0); - - $: amountFiat = parseFloat((((received - tip) * rate) / sats).toFixed(2)); - $: pendingFiat = parseFloat((((pending - tip) * rate) / sats).toFixed(2)); - $: tipFiat = parseFloat(((tip * rate) / sats).toFixed(2)); </script> <div class="container px-4 text-center mx-auto"> @@ -25,15 +21,20 @@ {#if received} <h1 class="text-3xl md:text-4xl font-bold mb-6">{$t('invoice.paymentSuccessful')}</h1> <h2 class="text-2xl md:text-3xl font-semibold"> - {f(amountFiat, currency)} + {f(fiat(received - tip, rate), currency)} {#if tip} - + {f(tipFiat, currency)} + <span class="text-lg"> + + {f(fiat(tip, rate), currency)} + </span> {/if} </h2> <h3 class="text-secondary md:text-lg mb-6 mt-1"> - {sat(received - tip)} + ⚡️{s(received - tip)} + {#if tip} - +{sat(tip)} + <span class="text-lg"> + + ⚡️{s(tip)} + </span> {/if} </h3> {/if} @@ -41,15 +42,17 @@ {#if pending} <h1 class="text-3xl md:text-4xl font-bold mb-6">Payment detected</h1> <h2 class="text-2xl md:text-3xl font-semibold"> - {f(pendingFiat, currency)} + {f(fiat(pending, rate), currency)} {#if tip} - + {f(tipFiat, currency)} + + {f(fiat(tip, rate), currency)} {/if} </h2> <h3 class="text-secondary md:text-lg mb-6 mt-1"> - {sat(pending - tip)} + ⚡️{s(pending - tip)} {#if tip} - +{sat(tip)} + <span class="text-lg"> + + ⚡️{s(tip)} + </span> {/if} </h3> {/if} diff --git a/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte index d033c413..296ef9c6 100644 --- a/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte +++ b/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte @@ -3,7 +3,7 @@ import { browser } from '$app/environment'; import { invoice as inv, request } from '$lib/store'; import { copy, focus, f, sat, get, reverseFormat, s, sats } from '$lib/utils'; - import { Icon } from '$comp'; + import { Icon, Slider } from '$comp'; import { goto } from '$app/navigation'; import { t } from '$lib/translations'; import { enhance } from '$app/forms'; @@ -37,9 +37,6 @@ customInput.value = ''; } tipPercent = e.target.value; - - clearTimeout(timeout); - timeout = setTimeout(update, 800); }; let submitting; @@ -56,10 +53,6 @@ }; let submit; - let update = async () => { - await tick(); - //submit.click(); - }; export let data; $: refresh(data); @@ -123,16 +116,7 @@ {#if !showCustomAmount} <div class="my-2 text-xl">{Math.round(tipPercent)}%</div> - <input - id="slider" - type="range" - min="0" - max="100" - value={tipPercent} - class="px-0 py-0 my-5" - style="--bgPercent: {tipPercent}%;" - on:input={handleSlide} - /> + <Slider handle={handleSlide} bind:value={tipPercent} /> <div class="flex mb-8 text-lg"> <div> @@ -201,43 +185,6 @@ </div> <style> - #slider { - -webkit-appearance: none; - width: 100%; - height: 6px; - border-radius: 5px; - background: linear-gradient( - to right, - black 0%, - black var(--bgPercent), - #f5f7fa var(--bgPercent), - #f5f7fa 100% - ); - outline: none; - } - - #slider:active { - background-color: black; - } - - #slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 30px; - height: 30px; - border-radius: 50%; - background: black; - cursor: pointer; - } - - #slider::-moz-range-thumb { - width: 30px; - height: 30px; - border-radius: 50%; - background: black; - cursor: pointer; - } - .active { @apply !bg-black !text-white; } diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.server.js b/src/routes/(app)/[username]/messages/+page.server.js index 63116099..7b651389 100644 --- a/src/routes/(app)/[username]/(profile)/messages/+page.server.js +++ b/src/routes/(app)/[username]/messages/+page.server.js @@ -7,10 +7,13 @@ export async function load({ params, parent, url }) { let messages = []; try { - messages = await get(`/nostr/${user.pubkey}/${subject.pubkey}/${since}/messages`); + messages = await get(`/${user.pubkey}/${since}/messages`); + messages = messages + .filter((m) => m.recipient.id === subject.id || m.author.id === subject.id) + .sort((a, b) => a.created_at - b.created_at); } catch (e) { console.log(`failed to fetch nostr messages`, e); } - return { newMessages: messages }; + return { messages, user }; } diff --git a/src/routes/(app)/[username]/messages/+page.svelte b/src/routes/(app)/[username]/messages/+page.svelte new file mode 100644 index 00000000..7a10d605 --- /dev/null +++ b/src/routes/(app)/[username]/messages/+page.svelte @@ -0,0 +1,171 @@ +<script> + import { cubicInOut } from 'svelte/easing'; + import { browser } from '$app/environment'; + import { t } from '$lib/translations'; + import { format } from 'date-fns'; + import { scale, fade, fly } from 'svelte/transition'; + import { enhance } from '$app/forms'; + import { Avatar, Icon } from '$comp'; + import { back, fail, focus } from '$lib/utils'; + import { sign, send, encrypt, decrypt } from '$lib/nostr'; + import { event as e, password } from '$lib/store'; + import { tick, onMount } from 'svelte'; + import { calculateId } from 'nostr'; + + export let data; + + function fadeScale(node, { delay = 0, duration = 200, easing = (x) => x, baseScale = 0 }) { + const o = +getComputedStyle(node).opacity; + const m = getComputedStyle(node).transform.match(/scale\(([0-9.]+)\)/); + const s = m ? m[1] : 1; + const is = 1 - baseScale; + + return { + delay, + duration, + css: (t) => { + const eased = easing(t); + return `opacity: ${eased * o}; transform: scale(${eased * s * is + baseScale})`; + } + }; + } + + let { messages, subject, user } = data; + let input, pane; + + $: initialize($password); + let initialize = async (p) => { + if (!p) return; + await Promise.all( + messages.map(async (event) => (event.message = await decrypt({ event, user }))) + ); + + messages = messages; + tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); + }; + + e.subscribe(async (event) => { + let found = ~messages.findIndex((m) => m.id === event?.id); + if (event?.recipient.id === user.id && !found) { + event.message = await decrypt({ event, user }); + messages.push(event); + messages = messages; + tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); + } + }); + + let sent, submitting, message; + let submit = async () => { + submitting = true; + + let event = { + pubkey: user.pubkey, + created_at: Math.floor(Date.now() / 1000), + kind: 4, + tags: [['p', subject.pubkey]] + }; + + event.message = message; + + message = ''; + + event.author = user; + event.recipient = subject; + + messages.push(event); + messages = messages; + tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); + + try { + event.id = await calculateId(event); + event.content = await encrypt({ message: event.message, recipient: subject.pubkey, user }); + await sign({ event, user }); + await send(event); + + sent = true; + } catch (e) { + console.log(e); + fail('Failed to send message'); + } + + message = ''; + submitting = false; + }; + + let keydown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + submit(); + } + }; +</script> + +<div class="container max-w-xl mx-auto px-4 space-y-5"> + <div + class="h-[50vh] max-h-[50vh] overflow-y-scroll scrollbar-thin scrollbar-thumb-[#F2F6FC] scrollbar-track-white pr-8" + bind:this={pane} + > + {#each messages as { id, author, message, created_at, pubkey }} + {@const ours = pubkey === user.pubkey} + {@const theirs = !ours} + + {#if message} + <div + class="flex gap-2" + class:flex-row-reverse={theirs} + class:justify-end={theirs} + in:fadeScale={{ + easing: cubicInOut, + baseScale: 0.5 + }} + > + <div + class="rounded-2xl px-4 py-2 max-w-[300px] mb-1 text-lg" + class:ours + class:theirs + :key={id} + > + {message} + </div> + <div class="mt-auto"> + <Avatar user={author} size={'12'} /> + </div> + </div> + <div class="text-sm text-gray-400 mb-6" class:text-right={ours}> + {format(new Date(created_at * 1000), 'MMM d')}, + {format(new Date(created_at * 1000), 'h:mm aa')} + </div> + {/if} + {/each} + </div> + + <form + method="POST" + class="w-full border rounded-xl outline-none gap-4 flex p-0 pr-2" + on:submit|preventDefault={submit} + > + <input type="hidden" name="requester_id" value={user.id} /> + <input type="hidden" name="recipient" value={subject.username} /> + + <div + use:focus + contenteditable + class="grow break-all py-4 outline-none mt-0 pl-4" + bind:innerHTML={message} + on:keydown={keydown} + /> + <button type="submit" class="my-auto shrink-0"> + <Icon icon="send" style="w-8" /> + </button> + </form> +</div> + +<style> + .ours { + @apply bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] text-black rounded-br-none ml-auto; + } + + .theirs { + @apply bg-gray-100 text-black rounded-bl-none; + } +</style> diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/[username]/payments/[...page]/+page.svelte index 142be4c4..a502f8c3 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte +++ b/src/routes/(app)/[username]/payments/[...page]/+page.svelte @@ -1,7 +1,7 @@ <script> import { Avatar, Icon } from '$comp'; import { onMount } from 'svelte'; - import { format, parseISO } from 'date-fns'; + import { format } from 'date-fns'; import { newPayment, payments } from '$lib/store'; import { t } from '$lib/translations'; import { get, f, s, sat, sats } from '$lib/utils'; @@ -180,14 +180,12 @@ </div> </a> {:else if p.with} - <a href={`/${p.with.username}`}> - <div class="flex"> - <div class="my-auto"> - <Avatar user={p.with} size={12} /> - </div> - <div class="my-auto ml-1 text-secondary">{p.with.username}</div> + <div class="flex"> + <div class="my-auto"> + <Avatar user={p.with} size={16} disabled={true} /> </div> - </a> + <div class="my-auto ml-1 text-secondary">{p.with.username}</div> + </div> {:else if p.type === types.classic} <img src="/images/classic.png" diff --git a/src/routes/(app)/[username]/receive/[...param]/+page.svelte b/src/routes/(app)/[username]/receive/[...param]/+page.svelte index 99d95be0..95495368 100644 --- a/src/routes/(app)/[username]/receive/[...param]/+page.svelte +++ b/src/routes/(app)/[username]/receive/[...param]/+page.svelte @@ -46,10 +46,5 @@ > {amount ? $t('payments.next') : $t('payments.sendersChoice')} </button> - - <div class="w-full flex justify-center"> - <span class="font-bold mr-2">{type[0].toUpperCase() + type.substring(1, type.length)}</span> - <Toggle id="prompt" checked={lightning} bind:value={lightning} /> - </div> </div> </form> diff --git a/src/routes/(app)/[username]/settings/[...tab]/+page.server.js b/src/routes/(app)/[username]/settings/[...tab]/+page.server.js index 361a9f85..535c588f 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/+page.server.js +++ b/src/routes/(app)/[username]/settings/[...tab]/+page.server.js @@ -1,5 +1,5 @@ import { fail, redirect } from '@sveltejs/kit'; -import { auth, get, post } from '$lib/utils'; +import { fd, auth, get, post } from '$lib/utils'; export function load({ params, url }) { if (url.pathname.endsWith('settings')) throw redirect(307, url.pathname + '/account'); @@ -7,8 +7,9 @@ export function load({ params, url }) { } export const actions = { - save: async ({ cookies, request }) => { - let form = Object.fromEntries(await request.formData()); + default: async ({ cookies, request }) => { + let form = await fd(request); + form.prompt = form.prompt === 'on'; let user = { ...(await get('/me', auth(cookies))), ...form }; try { diff --git a/src/routes/(app)/buy/+page.server.js b/src/routes/(app)/buy/+page.server.js new file mode 100644 index 00000000..fc7ae473 --- /dev/null +++ b/src/routes/(app)/buy/+page.server.js @@ -0,0 +1,27 @@ +import { env } from '$env/dynamic/private'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { post, auth } from '$lib/utils'; +import { createTicket } from '$lib/ticket'; + +export const actions = { + default: async ({ cookies, request }) => { + let form = await request.formData(); + + let amount = parseFloat(form.get('amount')); + if (!amount || amount < 0 || amount > 500) return fail(400, { message: 'Invalid amount' }); + + await post( + '/buy', + { + amount: form.get('amount'), + number: form.get('number').replace(/\s+/g, ''), + month: form.get('expiry').slice(0, 2), + year: form.get('expiry').slice(-2), + cvc: form.get('cvc') + }, + auth(cookies) + ); + + throw redirect(307, `/${form.get('username')}/payments`); + } +}; diff --git a/src/routes/(app)/buy/+page.svelte b/src/routes/(app)/buy/+page.svelte new file mode 100644 index 00000000..2f556ada --- /dev/null +++ b/src/routes/(app)/buy/+page.svelte @@ -0,0 +1,150 @@ +<script> + import { enhance } from '$app/forms'; + import { Icon, Lock, Card, Calendar, Spinner } from '$comp'; + import { animatedRate, selectedRate } from '$lib/store'; + import { f } from '$lib/utils'; + + export let data, form; + + let { username, currency } = data.user; + let loading; + + let amount, n, d, cvc; + + let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t); + let i; + + let o = $selectedRate; + $: animate($selectedRate); + let animate = (n) => { + clearInterval(i); + + let t = 0; + let d = o - n; + + i = setInterval(() => { + $animatedRate = (o - d * ease(t / 100)).toFixed(2); + if (t > 80) (o = n) && clearInterval(i); + t++; + }, 10); + }; +</script> + +<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5"> + <div class="w-full flex"> + <a href="/" class="mx-auto"> + <Icon icon="logo" /> + </a> + </div> + + <div class="flex justify-center items-center"> + <div class="shadow-xl rounded-3xl px-10 pt-5 pb-10 space-y-5 w-full mx-5"> + <div class="relative"> + <h1 class="text-2xl md:text-3xl font-semibold text-center">Buy Bitcoin</h1> + </div> + <p class="text-secondary text-center">Purchase up to $500 worth of Bitcoin</p> + + {#if form?.message} + <div class="text-red-600 text-center"> + {form.message} + </div> + {/if} + + <form + method="POST" + class="flex flex-wrap gap-5 w-full p-5 mt-20" + use:enhance={() => { + loading = true; + return ({ update }) => update() && (loading = false); + }} + > + <input type="hidden" name="username" value={username} /> + + <label class="relative w-full flex flex-col"> + <span class="font-bold mb-3">Amount</span> + <input + class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300" + type="text" + name="amount" + placeholder="•••" + bind:value={amount} + /> + <span + class="absolute bottom-[9px] left-0 translate-x-1/2 w-6 text-black peer-placeholder-shown:text-gray-300 + ">$</span + > + </label> + + <label class="relative w-full flex flex-col"> + <span class="font-bold mb-3">Card number</span> + <input + class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300" + type="text" + name="number" + placeholder="0000 0000 0000 0000" + bind:value={n} + /> + <Card /> + </label> + + <label class="relative flex-1 flex flex-col"> + <span class="font-bold mb-3">Expire date</span> + <input + class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300" + type="text" + name="expiry" + placeholder="MM/YY" + bind:value={d} + /> + <Calendar /> + </label> + + <label class="relative flex-1 flex flex-col"> + <span class="font-bold flex items-center gap-3 mb-3"> + CVC/CVV + <span class="relative group"> + <span + class="hidden group-hover:flex justify-center items-center px-2 py-1 text-xs absolute -right-2 transform translate-x-full -translate-y-1/2 w-max top-1/2 bg-black text-white" + > + 3-digit number on the back of your card</span + > + </span> + </span> + <input + class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300" + type="text" + name="cvc" + placeholder="•••" + bind:value={cvc} + /> + <Lock /> + </label> + + <div class="w-full flex mt-8"> + <button + class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-xl" + type="submit" + disabled={loading} + > + {#if loading} + <Spinner /> + {:else} + Submit + {/if} + </button> + </div> + </form> + + <div class="flex"> + <div class="text-secondary flex mr-auto"> + <div class="flex mr-1"> + <div class="my-auto mr-1">1</div> + <img src="/images/bitcoin.svg" class="w-5 my-auto" alt="Bitcoin" /> + </div> + <div>= {f($animatedRate, currency)}</div> + </div> + <p class="text-secondary">3% credit card fee applies</p> + </div> + </div> + </div> +</div> diff --git a/src/routes/(app)/launch/thanks/[asset]/+page.server.js b/src/routes/(app)/launch/thanks/[asset]/+page.server.js deleted file mode 100644 index e9e01376..00000000 --- a/src/routes/(app)/launch/thanks/[asset]/+page.server.js +++ /dev/null @@ -1 +0,0 @@ -// export const load = ({ parent }) => parent(); diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.js b/src/routes/(app)/ln/[lnurl]/+page.server.js index a12b063c..ddd714bf 100644 --- a/src/routes/(app)/ln/[lnurl]/+page.server.js +++ b/src/routes/(app)/ln/[lnurl]/+page.server.js @@ -1,4 +1,4 @@ -import { auth, get, post } from '$lib/utils'; +import { fd, auth, get, post } from '$lib/utils'; import { error, fail, redirect } from '@sveltejs/kit'; export async function load({ params, parent }) { @@ -24,10 +24,7 @@ export const actions = { pay: async ({ cookies, fetch, request }) => { let error; - let { callback, amount, minSendable, maxSendable } = Object.fromEntries( - await request.formData() - ); - + let { callback, amount, minSendable, maxSendable } = await fd(request); minSendable = Math.round(minSendable / 1000); maxSendable = Math.round(maxSendable / 1000); @@ -42,8 +39,9 @@ export const actions = { withdraw: async ({ cookies, fetch, request }) => { let error; - let { callback, amount, username, currency, minWithdrawable, maxWithdrawable, k1 } = - Object.fromEntries(await request.formData()); + let { callback, amount, username, currency, minWithdrawable, maxWithdrawable, k1 } = await fd( + request + ); minWithdrawable = Math.round(minWithdrawable / 1000); maxWithdrawable = Math.round(maxWithdrawable / 1000); diff --git a/src/routes/(app)/login/+page.server.js b/src/routes/(app)/login/+page.server.js index a6a7120a..67f686e0 100644 --- a/src/routes/(app)/login/+page.server.js +++ b/src/routes/(app)/login/+page.server.js @@ -1,5 +1,5 @@ import { fail, redirect } from '@sveltejs/kit'; -import { login } from '$lib/utils'; +import { login, fd } from '$lib/utils'; export const load = async ({ parent }) => { let { user } = await parent(); @@ -8,7 +8,7 @@ export const load = async ({ parent }) => { export const actions = { default: async ({ cookies, request }) => { - let form = Object.fromEntries(await request.formData()); + let form = await fd(request); let { username, password, token, loginRedirect } = form; let user = { username, password, token }; diff --git a/src/routes/(app)/login/+page.svelte b/src/routes/(app)/login/+page.svelte index 9a1e41da..75998cd0 100644 --- a/src/routes/(app)/login/+page.svelte +++ b/src/routes/(app)/login/+page.svelte @@ -1,5 +1,4 @@ <script> - import { messages } from '$lib/store'; import { RegisterOrLogin } from '$comp'; export let form; diff --git a/src/routes/(app)/payment/[id]/+page.server.js b/src/routes/(app)/payment/[id]/+page.server.js index cbd7355e..1c90ab69 100644 --- a/src/routes/(app)/payment/[id]/+page.server.js +++ b/src/routes/(app)/payment/[id]/+page.server.js @@ -1,6 +1,6 @@ import { auth, get } from '$lib/utils'; export async function load({ cookies, params }) { - let tx = await get(`/payments/${params.id}`, auth(cookies)); - return { tx }; + let payment = await get(`/payments/${params.id}`, auth(cookies)); + return { payment }; } diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte index a961d14d..2e170a43 100644 --- a/src/routes/(app)/payment/[id]/+page.svelte +++ b/src/routes/(app)/payment/[id]/+page.svelte @@ -1,62 +1,98 @@ <script> import { t } from '$lib/translations'; - import { back, copy, f, s, sats } from '$lib/utils'; + import { back, copy, f, s, fiat, sats } from '$lib/utils'; import { Avatar, Icon } from '$comp'; import { format } from 'date-fns'; import { PUBLIC_EXPLORER as expl } from '$env/static/public'; export let data; - let { user, tx } = data; + let { user, payment: p } = data; let { username } = user; - let { id, amount, created, rate, type, ref, fee, currency } = tx; - fee = fee || 0; + let { id, amount, created, rate, type, ref, tip, ourfee, fee, currency } = p; + let a = Math.abs(amount); - let fiat = (Math.abs(amount) * rate) / sats; + fee = fee || 0; </script> <button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={back}> <Icon icon="arrow-left" style="w-10" /> </button> -<div class="container mx-auto max-w-lg px-4 space-y-2 sm:space-y-5 break-all"> +<div class="container mx-auto max-w-lg px-4 space-y-8 break-all text-2xl"> <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10"> {$t(amount < 0 ? 'payments.sent' : 'payments.received')} </h1> + {#if p.with} + <div> + <span class="text-lg text-secondary my-auto mr-2">{amount > 0 ? 'From' : 'To'}</span> + <a href={`/${p.with.username}`}> + <div class="flex"> + <div class="my-auto"> + <Avatar user={p.with} size={20} /> + </div> + <div class="my-auto ml-1">{p.with.username}</div> + </div> + </a> + </div> + {/if} + <div> - <div class="font-bold">{$t('payments.amount')}</div> - <div class="text-xl"> - {f(fiat, currency)} - <span class="text-secondary">⚡️{`${s(Math.abs(amount) + fee)}`}</span> + <span class="text-lg text-secondary">Amount</span> + <div> + {f(fiat(a, rate), currency)} + {#if tip} + <span class="text-lg"> + + {f(fiat(tip, rate), currency)} + </span> + {/if} + <span class="text-secondary" + >⚡️{`${s(a)}`} + + {#if tip} + <span class="text-lg"> + + ⚡️{s(tip)} + </span> + {/if} + </span> </div> </div> <div> - <div class="font-bold">{$t('payments.date')}</div> + <span class="text-lg text-secondary">Network fee</span> <div> - {format(new Date(created), 'MMMM d')}, - {format(new Date(created), 'h:mm aaa')} + {f(fiat(fee, rate), currency)} + <span class="text-secondary">⚡️{`${s(fee)}`}</span> </div> </div> - {#if type === 'lightning'} + {#if ourfee > 0} <div> - <div class="font-bold">{$t('payments.preimage')}</div> + <span class="text-lg text-secondary">Platform fee</span> <div> - {ref} + {f(fiat(ourfee, rate), currency)} + <span class="text-secondary">⚡️{`${s(ourfee)}`}</span> </div> </div> - {:else} + {/if} + + <div> + <span class="text-lg text-secondary">Date</span> + <div> + {format(new Date(created), 'MMMM d')}, + {format(new Date(created), 'h:mm aaa')} + </div> + </div> + + {#if type === 'bitcoin'} <div> - <div class="font-bold">{$t('payments.id')}</div> + <span class="text-lg text-secondary">Txid</span> <div class="flex"> <div> - <a href={`${expl}/tx/${id}`} target="_blank" rel="noreferrer" class="text-blue-600" - >{id}</a - > + <a href={`${expl}/tx/${id}`} target="_blank" rel="noreferrer">{id}</a> </div> - <button class="flex font-bold hover:opacity-80 mb-auto" on:click={() => copy(id)} - ><Icon icon="copy" style="mr-1" /> + <button class="flex font-bold hover:opacity-80 mb-auto my-auto" on:click={() => copy(id)} + ><Icon icon="copy" style="ml-2 w-20 my-auto" /> </button> </div> </div> diff --git a/src/routes/(app)/pot/[name]/withdraw/+page.server.js b/src/routes/(app)/pot/[name]/withdraw/+page.server.js index ab56867d..ac33e940 100644 --- a/src/routes/(app)/pot/[name]/withdraw/+page.server.js +++ b/src/routes/(app)/pot/[name]/withdraw/+page.server.js @@ -1,5 +1,5 @@ import { fail, redirect } from '@sveltejs/kit'; -import { auth, post } from '$lib/utils'; +import { fd, auth, post } from '$lib/utils'; export async function load({ parent }) { let { user } = await parent(); @@ -8,8 +8,7 @@ export async function load({ parent }) { export const actions = { default: async ({ cookies, request }) => { - let body = Object.fromEntries(await request.formData()); - + let body = await fd(request); try { await post('/take', body, auth(cookies)); } catch (e) { diff --git a/src/routes/(app)/register/+page.server.js b/src/routes/(app)/register/+page.server.js index 7799d5ae..e71b2a60 100644 --- a/src/routes/(app)/register/+page.server.js +++ b/src/routes/(app)/register/+page.server.js @@ -1,5 +1,5 @@ import { fail, redirect } from '@sveltejs/kit'; -import { post, login } from '$lib/utils'; +import { fd, post, login } from '$lib/utils'; export const load = async ({ parent }) => { let { user } = await parent(); @@ -8,7 +8,7 @@ export const load = async ({ parent }) => { export const actions = { default: async ({ cookies, request }) => { - let form = Object.fromEntries(await request.formData()); + let form = await fd(request); let { username, password, cipher, salt, pubkey, loginRedirect } = form; let user = { username, password, cipher, salt, pubkey }; let error; diff --git a/src/routes/(app)/send/[...text]/+page.server.js b/src/routes/(app)/send/[...text]/+page.server.js index d09b4da9..9c7d4185 100644 --- a/src/routes/(app)/send/[...text]/+page.server.js +++ b/src/routes/(app)/send/[...text]/+page.server.js @@ -45,14 +45,15 @@ let parse = async (t, host) => { // bitcoin if (validate(t)) { try { - ({ id } = await get(`/invoice/${t}`)); + ({ id, user } = await get(`/invoice/${t}`)); } catch (e) { let r = `/send/bitcoin/${t}`; if (amount) r += '/' + amount; throw redirect(307, r); } - if (id) throw redirect(307, `/send/${id}`); + if (user) throw redirect(307, `/send/${user.username}`); + else if (id) throw redirect(307, `/send/${id}`); } // user diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte index 5a376f9f..3abc5a1a 100644 --- a/src/routes/(app)/send/[...text]/+page.svelte +++ b/src/routes/(app)/send/[...text]/+page.svelte @@ -7,7 +7,6 @@ import { t } from '$lib/translations'; import { AppHeader, Avatar, Icon, Spinner } from '$comp'; import { back, fail } from '$lib/utils'; - import { format, parseISO } from 'date-fns'; export let data; export let form; @@ -99,7 +98,7 @@ <div> {#each contacts as c} <a href={`/send/${c.username}`}> - <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> + <div class="border-b p-2 last:border-b-0 hover:bg-gray-100 rounded-2xl"> <div class="flex"> <div> <div class="flex"> diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js index 59e9326d..92a754ae 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js +++ b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js @@ -1,23 +1,29 @@ import { fail, redirect } from '@sveltejs/kit'; -import { auth, post } from '$lib/utils'; +import { auth, post, fd } from '$lib/utils'; let seen = {}; export const actions = { default: async ({ cookies, request }) => { try { - let feeRate, subtract; - let { address, amount, pin, ts } = Object.fromEntries(await request.formData()); - + let { address, amount, confirmed, feeRate, subtract, pin, stale, ts } = await await fd( + request + ); if (seen[ts]) return fail(400, { message: 'Duplicate form submission' }); - seen[ts] = 1; - setTimeout(() => delete seen[ts], 30000); - let { tx } = await post( + let estimate = await post( '/bitcoin/fee', { address, amount, feeRate, subtract }, auth(cookies) ); - await post('/bitcoin/send', { pin, tx }, auth(cookies)); + + let { tx } = estimate; + + if (stale || !confirmed) return fail(400, estimate); + + seen[ts] = 1; + setTimeout(() => delete seen[ts], 30000); + + await post('/bitcoin/send', { pin, subtract, tx }, auth(cookies)); } catch (e) { return fail(400, { message: e.message }); } diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte index 6b216cb3..77d5fde6 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte @@ -1,24 +1,48 @@ <script> + import { tick } from 'svelte'; import { t } from '$lib/translations'; import { enhance } from '$app/forms'; - import { Icon, Numpad, Spinner } from '$comp'; + import { Toggle, Slider, Icon, Numpad, Spinner } from '$comp'; import { page } from '$app/stores'; - import { back } from '$lib/utils'; - import { pin } from '$lib/store'; + import { fiat as toFiat, f, back, s, sat } from '$lib/utils'; + import { pin, selectedRate } from '$lib/store'; export let data; export let form; let { address, amount } = $page.params; - let { currency } = data.user; - let loading, submit; + let { balance, currency } = data.user; + let loading, submit, fiat, confirmed, feeRate, min, max, fee, ourfee, stale, subtract, adjusting; let toggle = () => (loading = true); + let toggleAdjusting = () => (adjusting = !adjusting); $: update(form); let update = () => { - if (form?.message.includes('pin')) $pin = undefined; loading = false; + if (form?.message?.includes('pin')) $pin = undefined; + if (form) ({ min, max, feeRate, fee, ourfee, subtract } = form); + if (!ourfee) ourfee = 0; + if (feeRate) confirmed = true; + stale = false; + }; + + let setMax = () => { + fiat = false; + amount = balance; + subtract = true; + }; + + $: if (amount + fee > balance) subtract = true; + + let timeout; + let handle = (e) => { + stale = true; + feeRate = e.target.value; + clearTimeout(timeout); + timeout = setTimeout(() => { + tick().then(() => submit.click()); + }, 200); }; </script> @@ -32,30 +56,82 @@ </div> {/if} -<div class="container px-4 mt-20 max-w-xl mx-auto"> - <div class="text-center mb-8"> - <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t('payments.sendTo')}</h1> - <p class="text-lg text-secondary">{address}</p> - </div> - <Numpad bind:amount {currency} {submit} /> +<div class="container px-4 mt-20 max-w-xl mx-auto space-y-5 text-center"> + <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t('payments.send')}</h1> + + <div class="text-lg text-secondary break-all">{address}</div> + + {#if confirmed} + <div class="text-center"> + <h2 class="text-2xl md:text-3xl font-semibold"> + {f(toFiat(amount, $selectedRate), currency)} + </h2> + <h3 class="text-secondary md:text-lg mb-6 mt-1">⚡️{s(amount)}</h3> + </div> + + <div class="text-center"> + <h2 class="text-secondary mb-5 text-lg">with fee</h2> + + <div class="relative"> + <h2 class="text-xl font-semibold"> + {f(toFiat(fee + ourfee, $selectedRate), currency)} + </h2> + <h3 class="text-secondary mb-6 mt-1">⚡️{s(fee + ourfee)}</h3> + <button class="absolute right-1/4 top-0 text-secondary p-4" on:click={toggleAdjusting} + ><Icon icon="settings" style="opacity-30" /></button + > + </div> + </div> + {:else} + <Numpad bind:amount bind:fiat {currency} {submit} /> + {/if} <form method="POST" use:enhance on:submit={toggle}> <input name="ts" value={Date.now()} type="hidden" /> <input name="address" value={address} type="hidden" /> <input name="amount" value={amount} type="hidden" /> <input name="pin" value={$pin} type="hidden" /> + <input name="confirmed" value={confirmed} type="hidden" /> + <input name="feeRate" value={feeRate} type="hidden" /> + <input name="stale" value={stale} type="hidden" /> + <input name="subtract" value={subtract} type="hidden" /> + + {#if confirmed && adjusting} + <div class="relative mt-10 mb-20"> + <div class="absolute top-5 left-0 text-secondary">Slower (~40 blocks)</div> + <div class="absolute top-5 right-0 text-secondary">Faster (~1 block)</div> + <Slider bind:value={feeRate} {handle} {min} {max} /> + </div> + <div class="flex justify-center gap-2 mb-10"> + <div class="my-auto"> + <Toggle id="subtract" checked={subtract} bind:value={subtract} /> + </div> + <label for="subtract" class="text-secondary text-lg my-auto mb-2" + >Take fee from amount</label + > + </div> + {/if} + + <div class="flex justify-center gap-2"> + {#if !confirmed} + <button + type="button" + class="hover:opacity-80 bg-black text-white rounded-2xl py-3 px-4 mt-2 border font-bold" + on:click|preventDefault={setMax} + on:keydown={setMax}>Max ⚡️{s(balance)}</button + > + {/if} - <div class="flex w-full"> <button 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="opacity-100 hover:opacity-80 rounded-2xl border py-3 font-bold mt-2 bg-black text-white px-4 w-24" disabled={loading} > - {#if loading} - <Spinner /> - {:else} + {#if confirmed} {$t('payments.send')} + {:else} + {$t('payments.next')} {/if} </button> </div> diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.js b/src/routes/(app)/send/invoice/[id]/+page.server.js index 2df19985..3db03123 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.server.js +++ b/src/routes/(app)/send/invoice/[id]/+page.server.js @@ -1,5 +1,5 @@ import { error, fail, redirect } from '@sveltejs/kit'; -import { btc as asset, auth, get, post } from '$lib/utils'; +import { fd, auth, get, post } from '$lib/utils'; export async function load({ params: { id }, parent }) { let { user } = await parent(); @@ -13,15 +13,10 @@ export async function load({ params: { id }, parent }) { export const actions = { default: async ({ cookies, params: { hash }, request }) => { try { - let body = Object.fromEntries(await request.formData()); - let { confirmed } = body; + let body = await fd(request); body.amount = parseInt(body.amount); body.hash = hash; - if (!confirmed) { - return fail(400, { amount, confirm: true }); - } - await post('/payments', body, auth(cookies)); } catch (e) { return fail(400, { message: e.message }); diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte index 27837bca..7570a61a 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.svelte +++ b/src/routes/(app)/send/invoice/[id]/+page.svelte @@ -69,7 +69,6 @@ <input name="amount" value={amount} type="hidden" /> <input name="payreq" value={payreq} type="hidden" /> <input name="username" value={recipient.username} type="hidden" /> - <input name="confirmed" value={form?.confirm} type="hidden" /> <input name="pin" value={$pin} type="hidden" /> <div class="flex w-full"> diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.server.js b/src/routes/(app)/send/lightning/[payreq]/+page.server.js index 90e6c0cc..94231376 100644 --- a/src/routes/(app)/send/lightning/[payreq]/+page.server.js +++ b/src/routes/(app)/send/lightning/[payreq]/+page.server.js @@ -1,23 +1,17 @@ import { fail, redirect } from '@sveltejs/kit'; -import { btc as asset, auth, post } from '$lib/utils'; +import { fd, auth, post } from '$lib/utils'; export async function load({ params }) { return post('/parse', params); } export const actions = { - setAmount: async ({ cookies, request }) => { - return Object.fromEntries(await request.formData()); - }, + setAmount: async ({ cookies, request }) => fd(request), send: async ({ cookies, request }) => { try { - let body = Object.fromEntries(await request.formData()); - let { amount, confirmed } = body; - - if (!confirmed) { - return fail(400, { amount, confirm: true }); - } + let body = await fd(request); + let { amount } = body; await post('/payments', body, auth(cookies)); } catch (e) { diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/+page.svelte index 6ae699d2..af07a565 100644 --- a/src/routes/(app)/send/lightning/[payreq]/+page.svelte +++ b/src/routes/(app)/send/lightning/[payreq]/+page.svelte @@ -13,10 +13,11 @@ let { alias } = data; let { currency } = data.user; - let amount = form?.amount || data.amount; let a; let maxfee = 100; + $: amount = form?.amount || data.amount; + let loading; let submit = () => (loading = true); $: update(form); @@ -53,7 +54,6 @@ <form method="POST" use:enhance on:submit={submit} action="?/send" class="space-y-5"> <input name="payreq" value={payreq} type="hidden" /> <input name="amount" value={amount} type="hidden" /> - <input name="confirmed" value={form?.confirm} type="hidden" /> <input name="pin" value={$pin} type="hidden" /> <input name="maxfee" value={maxfee} type="hidden" /> @@ -89,7 +89,7 @@ </div> {/if} {:else} - <form method="POST" action="?/setAmount" class="w-[300px] mx-auto"> + <form method="POST" action="?/setAmount" class="w-[300px] mx-auto" use:enhance> <input type="hidden" value={a} name="amount" /> <Numpad bind:amount={a} {currency} /> <button diff --git a/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js b/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js index 0e1b7f5b..6c11811c 100644 --- a/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js +++ b/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js @@ -3,7 +3,7 @@ import { auth, post } from '$lib/utils'; export const actions = { default: async ({ cookies, request }) => { - let body = Object.fromEntries(await request.formData()); + let body = await fd(request); let p; try { diff --git a/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte b/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte index bb11ddde..da761cc2 100644 --- a/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte @@ -16,7 +16,7 @@ let submit = () => (loading = true); $: update(form); let update = () => { - if (form?.message.includes('pin')) $pin = undefined; + if (form?.message?.includes('pin')) $pin = undefined; loading = false; }; </script> diff --git a/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js b/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js index 42b47c8f..18eda839 100644 --- a/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js +++ b/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js @@ -1,5 +1,5 @@ import { error, fail, redirect } from '@sveltejs/kit'; -import { btc as asset, auth, get, post } from '$lib/utils'; +import { fd, auth, get, post } from '$lib/utils'; export async function load({ params, parent }) { let { user } = await parent(); @@ -12,14 +12,10 @@ export async function load({ params, parent }) { export const actions = { default: async ({ cookies, request }) => { try { - let body = Object.fromEntries(await request.formData()); - let { amount, confirmed } = body; + let body = await fd(request); + let { amount } = body; amount = parseInt(amount); - if (!confirmed) { - return fail(400, { amount, confirm: true }); - } - await post('/payments', body, auth(cookies)); } catch (e) { console.log(e); diff --git a/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte b/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte index 9704e3c6..74da1fa8 100644 --- a/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte @@ -16,10 +16,11 @@ let amount = form?.amount || data.amount; let a, af, - amountFiat = amount * ($selectedRate / sats), fiat = !amount, hash; + $: amountFiat = amount * ($selectedRate / sats); + let setAmount = async () => { amount = a; amountFiat = af; @@ -39,7 +40,7 @@ $: update(form); let update = () => { - if (form?.message.includes('pin')) $pin = undefined; + if (form?.message?.includes('pin')) $pin = undefined; loading = false; }; </script> @@ -68,7 +69,6 @@ <form method="POST" use:enhance on:submit={submit}> <input name="amount" value={amount} type="hidden" /> <input name="username" value={subject.username} type="hidden" /> - <input name="confirmed" value={form?.confirm} type="hidden" /> <input name="pin" value={$pin} type="hidden" /> <input name="hash" value={hash} type="hidden" /> diff --git a/src/routes/(app)/sent/+page.svelte b/src/routes/(app)/sent/+page.svelte index 79a0aa6e..760d2e1d 100644 --- a/src/routes/(app)/sent/+page.svelte +++ b/src/routes/(app)/sent/+page.svelte @@ -1,5 +1,5 @@ <script> - import { f, s, sat, sats } from '$lib/utils'; + import { fiat, f, s, sat, sats } from '$lib/utils'; import { scale } from 'svelte/transition'; import { Icon } from '$comp'; import { toast } from '@zerodevx/svelte-toast'; @@ -7,13 +7,12 @@ export let data; let { payments, user } = data; + let { currency, username } = user; let payment = payments[0]; - let { amount, rate } = payment; + let { amount, rate, tip } = payment; amount = Math.abs(amount); - let fiat = (amount * rate) / sats; - toast.pop(0); </script> @@ -23,12 +22,25 @@ </div> <h1 class="text-3xl md:text-4xl font-bold mb-6">Sent!</h1> <h2 class="text-2xl md:text-3xl font-semibold"> - {f(fiat, user.currency)} + {f(fiat(amount, rate), currency)} + {#if tip} + <span class="text-lg"> + + {f(fiat(tip, rate), currency)} + </span> + {/if} </h2> - <h3 class="text-secondary md:text-lg mb-6 mt-1">{sat(amount)}</h3> + <h3 class="text-secondary md:text-lg mb-6 mt-1"> + ⚡️{s(amount)} + + {#if tip} + <span class="text-lg"> + + ⚡️{s(tip)} + </span> + {/if} + </h3> </div> -<a href={`/${user.username}/payments`}> +<a href={`/${username}/payments`}> <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" /> </a> diff --git a/src/routes/(app)/support/+page.server.js b/src/routes/(app)/support/+page.server.js deleted file mode 100644 index 5d3bf988..00000000 --- a/src/routes/(app)/support/+page.server.js +++ /dev/null @@ -1,16 +0,0 @@ -import { post } from '$lib/utils'; -import { fail } from '@sveltejs/kit'; - -export const actions = { - default: async ({ cookies, request }) => { - let form = Object.fromEntries(await request.formData()); - - try { - await post('/email', form); - } catch (e) { - return fail(400, { error: e.message }); - } - - return { success: true }; - } -}; diff --git a/src/routes/(app)/support/+page.svelte b/src/routes/(app)/support/+page.svelte index d00cc069..8dbfc1af 100644 --- a/src/routes/(app)/support/+page.svelte +++ b/src/routes/(app)/support/+page.svelte @@ -1,10 +1,10 @@ <script> - import { enhance } from '$app/forms'; + import { fail, success, post } from '$lib/utils'; import { scale } from 'svelte/transition'; import { AppHeader, Icon } from '$comp'; import { t } from '$lib/translations'; - export let data, form; + export let data; let { user } = data; $: if (user) { @@ -13,41 +13,58 @@ let email; let message; + let sent; + + let submit = (e) => { + e.preventDefault(); + grecaptcha.ready(() => { + grecaptcha + .execute('6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM', { action: 'submit' }) + .then((token) => + post('/support', { email, message, token }) + .then(() => (sent = true)) + .catch(() => fail('problem submitting')) + ); + }); + }; </script> +<svelte:head> + <script + src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM" + ></script> +</svelte:head> + {#if user} <AppHeader {data} /> {/if} -{#if form?.error} - <div class="text-red-600 text-center"> - {form.error} - </div> -{/if} +<div class="container px-4 max-w-lg mx-auto mt-20 space-y-8"> + {#if sent} + <h1 class="text-center text-3xl md:text-4xl font-semibold">Thank you!</h1> + <div class="text-center text-lg text-secondary">Someone will be in touch shortly.</div> -<div class="container px-4 max-w-lg mx-auto mt-20 space-y-5"> - {#if form?.success} - <h1 class="text-center text-3xl md:text-4xl font-semibold mb-8">Thank you!</h1> - <p class="text-center mb-8">Someone will be in touch shortly.</p> - - <a href={user ? `/${user.username}` : '/'}> - <button class="rounded-full border py-2 px-5 font-bold hover:opacity-80 w-full mb-2" - >Done</button - > - </a> + <div> + <a href={user ? `/${user.username}` : '/'}> + <button + class="rounded-full border py-2 px-5 font-bold hover:opacity-80 w-full bg-black text-white" + >Ok</button + > + </a> + </div> {:else} <h1 class="text-center text-3xl md:text-4xl font-semibold mb-8"> {$t('user.support.header')} </h1> - <p class="text-secondary"> + <p class="text-secondary text-lg"> Fill out this form or email us directly at <a class="underline" href="mailto:[email protected]">[email protected]</a > and we'll do our best to get back to you in a timely manner. </p> - <form method="POST" use:enhance> + <form on:submit={submit}> <div class="mb-4"> <label for="account" class="font-semibold">{$t('user.support.accountName')}</label> <input @@ -79,7 +96,7 @@ <button type="submit" disabled={!email || !message} - class="{!email || !message + class="g-recaptcha {!email || !message ? 'opacity-50' : 'opacity-100'} bg-black text-white font-bold rounded-xl py-3 w-full mx-auto {email && message diff --git a/src/routes/(app)/support/+server.js b/src/routes/(app)/support/+server.js new file mode 100644 index 00000000..6b1778a8 --- /dev/null +++ b/src/routes/(app)/support/+server.js @@ -0,0 +1,9 @@ +import { json } from '@sveltejs/kit'; +import { auth, post } from '$lib/utils'; + +export const prerender = false; + +export async function POST({ cookies, request }) { + let result = await post('/email', await request.json(), auth(cookies)); + return json(result); +} diff --git a/src/routes/(static)/+layout.server.js b/src/routes/(static)/+layout.server.js new file mode 100644 index 00000000..2deeebc5 --- /dev/null +++ b/src/routes/(static)/+layout.server.js @@ -0,0 +1,16 @@ +import { get, post, auth, protectedRoutes } from '$lib/utils'; + +export async function load({ cookies, request, url, params }) { + let token = cookies.get('token'); + let rate, + rates = { USD: 1 }; + + let user; + if (token) { + try { + user = await get('/me', auth(cookies)); + } catch (e) {} + } + + return { user }; +} diff --git a/src/routes/(static)/+layout.svelte b/src/routes/(static)/+layout.svelte index 6e31f17e..3d596dff 100644 --- a/src/routes/(static)/+layout.svelte +++ b/src/routes/(static)/+layout.svelte @@ -1,8 +1,12 @@ <script> + import { SvelteToast } from '@zerodevx/svelte-toast'; import { Footer, LandingHeader } from '$comp'; + export let data; + $: ({ user } = data); </script> -<LandingHeader /> +<SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> +<LandingHeader {user} /> <main class="container flex px-10 md:px-40 mx-auto min-h-[600px] pb-20"> <slot /> </main> diff --git a/src/routes/(static)/bounties/+page.svelte b/src/routes/(static)/bounties/+page.svelte index 140b9c16..e3670e78 100644 --- a/src/routes/(static)/bounties/+page.svelte +++ b/src/routes/(static)/bounties/+page.svelte @@ -1,27 +1,30 @@ <div class="space-y-5"> - <h1 class="text-3xl mb-6 pt-20">Bounties</h1> + <h1 class="text-3xl mb-6 pt-28 md:pt-12">Bounties</h1> - <p class="text-secondary"> + <p> We're offering <b>⚡️1,000,000</b> to the first <b>10 people</b> that help us onboard businesses in the following categories to take bitcoin payments: </p> - <ul class="text-xl ml-20"> - <li>Grocery or Produce Store</li> - <li>Cafe</li> + <ul> + <li>Grocery / Produce Store</li> + <li> + Cafe — Claimed by <a href="/reverendhodl">ReverendHodl</a> for + <a href="https://btcmap.org/map?lat=49.2814113&long=-123.049572">Laughing Bean Coffee Co.</a> + </li> <li>Bakery</li> <li>Butcher Shop</li> - <li>Laundromat or Dry Cleaner</li> - <li>Dollar Store, Book Store, or Gift Shop</li> + <li>Laundromat / Dry Cleaner</li> + <li>Dollar Store / Book Store / Gift Shop</li> <li>Sushi Restaurant</li> - <li>Pizza by the Slice Joint</li> - <li>Liquor Store or Gas Station</li> - <li>Bubble Tea or Ice Cream Parlour</li> + <li>Pizza by the Slice</li> + <li>Liquor Store / Gas Station</li> + <li>Bubble Tea / Ice Cream Parlour</li> </ul> - <h2 class="text-2xl">Terms and Conditions</h2> + <h2 class="text-xl">Terms and Conditions</h2> - <ul class="ml-20"> + <ul> <li>The business must have a physical location that takes walk-in customers</li> <li> They must be located within Vancouver, North Vancouver, West Vancouver, Burnaby, New @@ -51,7 +54,7 @@ <p> <b>⚡️750,000</b> will be awarded once the merchant has accepted bitcoin for 6 months. We'll be sending - <a href="https://btcmap.org/leaderboard" class="text-black hover:underline" target="_blank" + <a href="https://btcmap.org/leaderboard" class="text-black hover:underline" target="_blank" rel="noreferrer" >shadowy supertaggers</a > to check up on them periodically. </p> @@ -61,7 +64,7 @@ applicants without cause </p> - <h2 class="text-2xl">Ready to begin!?</h2> + <h2 class="text-xl">Ready to begin!?</h2> <p> To make a referral, reach out on one of our socials or email us at <a @@ -79,10 +82,7 @@ </div> <style> - p { - @apply text-secondary; - } - b { - @apply text-black text-xl; + a { + @apply font-bold; } </style> diff --git a/src/routes/(static)/docs/+page.server.js b/src/routes/(static)/docs/+page.server.js new file mode 100644 index 00000000..e80365fa --- /dev/null +++ b/src/routes/(static)/docs/+page.server.js @@ -0,0 +1,4 @@ +export let load = async ({ cookies }) => { + let token = cookies.get('token'); + return { token }; +}; diff --git a/src/routes/(static)/docs/+page.svelte b/src/routes/(static)/docs/+page.svelte index c634cd76..cb82bd05 100644 --- a/src/routes/(static)/docs/+page.svelte +++ b/src/routes/(static)/docs/+page.svelte @@ -1,17 +1,77 @@ -Coming soon -<!-- <h1>API Examples</h1> --> -<!-- <pre><code class="whitespace-pre-wrap" --> -<!-- >{` --> -<!-- api="https://coinos.io/api" --> -<!-- json="Content-Type: application/json" --> -<!-- --> -<!-- username=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) --> -<!-- password="pw" --> -<!-- --> -<!-- echo Create user $username with password $password --> -<!-- curl -s "$api/register" -H "$json" -d '{"user": {"username": "'$username'", "password": "'$password'", "confirm": "'$password'"}}' > /dev/null --> -<!-- echo "" --> -<!-- --> +<script> + import { copy } from '$lib/utils'; + import { Code, Icon } from '$comp'; + import { PUBLIC_COINOS_URL } from '$env/static/public'; + export let data; + let { token } = data; + let api = PUBLIC_COINOS_URL + '/api'; + + if (!token) token = '<login to see your token here>'; + let tokenSample = `export token="${token}"`; +</script> + +<div class="space-y-8 lg:text-xl mt-20 w-full max-w-full"> + <h1 class="text-4xl">API Documentation</h1> + + <p class="text-secondary"> + Coinos has a simple REST API that can be used to register accounts, fetch data, and make + payments + </p> + + <p class="text-secondary">* = required parameter</p> + + <h2 class="text-2xl">Base URL</h2> + <div class="bg-black text-white rounded-lg p-4 flex gap-4"> + <div>{api}</div> + <button class="ml-auto my-auto invert opacity-90" on:click={() => copy(api)} + ><Icon icon="copy" style="w-10 max-w-none" /></button + > + </div> + + <h2 class="text-2xl">Auth Token</h2> + <p class="text-secondary">This JWT authorizes you to use the API</p> + <div class="bg-black text-white rounded-lg p-4 flex gap-4"> + <div class="w-full break-all">{tokenSample}</div> + <button class="ml-auto my-auto invert opacity-90" on:click={() => copy(tokenSample)} + ><Icon icon="copy" style="w-10 max-w-none" /></button + > + </div> + + <h2 class="text-2xl">POST /register</h2> + <p class="text-secondary">Register a new user account with a username and password</p> + <Code sample="register" /> + + <h2 class="text-2xl">POST /login</h2> + <p class="text-secondary">Login to an account to get its auth token</p> + <Code sample="login" /> + + <h2 class="text-2xl">POST /invoice</h2> + <p class="text-secondary">Create an invoice.</p> + <div> + <div class="text-secondary"> + <span class="font-mono font-bold">type</span>: bitcoin or lightning + </div> + <div class="text-secondary"> + <span class="font-mono font-bold">amount</span>: amount in satoshis + </div> + <div class="text-secondary"> + <span class="font-mono font-bold">webhook</span>: an endpoint to hit when the invoice is paid + </div> + <div class="text-secondary"> + <span class="font-mono font-bold">secret</span>: a secret string for the webhook to check + </div> + </div> + + hey + <Code sample="invoice" /> + + <h2 class="text-2xl">GET /invoice/:hash</h2> + <p class="text-secondary"> + Fetch an invoice by passing a bitcoin address or lightning payment hash + </p> + <Code sample="fetchInvoice" /> +</div> + <!-- echo Login and get an API token --> <!-- token=$(curl -s "$api/taboggan" -H "$json" -d '{"username": "'$username'", "password": "'$password'"}' | jq -r .token) --> <!-- echo $token --> @@ -73,5 +133,5 @@ Coming soon <!-- --> <!-- curl -s "$api/bitcoin/send" -H "$json" -H "$auth" -d '{"address": "bc1qqc9azhdjjpdxenrndt8cw4tw9del5pyrjn0wln", "tx": { "hex": "'$hex'", "fee": "'$fee'" }}' | jq -r '{ amount, fee, balance: (.account.balance)}' --> <!-- echo "" --> -<!-- `}</code --> -<!-- ></pre> --> + +<!-- `} --> diff --git a/src/routes/+page.server.js b/src/routes/+page.server.js index 4bcc6089..7936e88b 100644 --- a/src/routes/+page.server.js +++ b/src/routes/+page.server.js @@ -1,6 +1,6 @@ import { get } from '$lib/utils'; -export const prerender = true; +// export const prerender = true; export let load = async () => { let data = { locations: [] }; |
