aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Barratt <[email protected]>2023-03-07 10:29:49 -0800
committerJohn Barratt <[email protected]>2023-03-07 10:29:49 -0800
commit8ef9568cd1bd822495ce005d4e4cfaa6cb0d92ce (patch)
tree4ea5530cc2b7e5d970dc8a84cacfecfe2cce46b7
parentc97fb43abba375920fff361ec5fa4eb64a5a0d3f (diff)
parent0f44e3283c25c9783a52cf20957661da49d2f90b (diff)
downloadcoinos-ui-notifications.tar.gz
coinos-ui-notifications.zip
Merge branch 'master' of https://github.com/coinos/coinos-ui into notificationsnotifications
-rw-r--r--package.json2
-rw-r--r--pnpm-lock.yaml61
-rw-r--r--src/app.css1
-rw-r--r--src/components/About.svelte13
-rw-r--r--src/components/AppHeader.svelte1
-rw-r--r--src/components/Balance.svelte2
-rw-r--r--src/components/Code.svelte31
-rw-r--r--src/components/LandingHeader.svelte26
-rw-r--r--src/components/Lnurlp.svelte2
-rw-r--r--src/components/Numpad.svelte55
-rw-r--r--src/components/Slider.svelte56
-rw-r--r--src/components/index.js2
-rw-r--r--src/lib/invoice.js2
-rw-r--r--src/lib/nostr.js14
-rw-r--r--src/lib/parse.js0
-rw-r--r--src/lib/samples.js23
-rw-r--r--src/lib/socket.js27
-rw-r--r--src/lib/store.js3
-rw-r--r--src/lib/translations/index.js5
-rw-r--r--src/lib/translations/lang.json3
-rw-r--r--src/lib/utils.js18
-rw-r--r--src/locales/en.json15
-rw-r--r--src/locales/es.json259
-rw-r--r--src/locales/fr.json84
-rw-r--r--src/locales/zh.json291
-rw-r--r--src/routes/(app)/+error.svelte38
-rw-r--r--src/routes/(app)/+layout.server.js2
-rw-r--r--src/routes/(app)/+layout.svelte2
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte3
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.server.js29
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte229
-rw-r--r--src/routes/(app)/[username]/(profile)/messages/+page.svelte123
-rw-r--r--src/routes/(app)/[username]/(profile)/request/+page.server.js6
-rw-r--r--src/routes/(app)/[username]/dashboard/+page.svelte1
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/+page.svelte75
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte29
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte57
-rw-r--r--src/routes/(app)/[username]/messages/+page.server.js (renamed from src/routes/(app)/[username]/(profile)/messages/+page.server.js)7
-rw-r--r--src/routes/(app)/[username]/messages/+page.svelte171
-rw-r--r--src/routes/(app)/[username]/payments/[...page]/+page.svelte14
-rw-r--r--src/routes/(app)/[username]/receive/[...param]/+page.svelte5
-rw-r--r--src/routes/(app)/[username]/settings/[...tab]/+page.server.js7
-rw-r--r--src/routes/(app)/buy/+page.server.js27
-rw-r--r--src/routes/(app)/buy/+page.svelte150
-rw-r--r--src/routes/(app)/launch/thanks/[asset]/+page.server.js1
-rw-r--r--src/routes/(app)/ln/[lnurl]/+page.server.js12
-rw-r--r--src/routes/(app)/login/+page.server.js4
-rw-r--r--src/routes/(app)/login/+page.svelte1
-rw-r--r--src/routes/(app)/payment/[id]/+page.server.js4
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte82
-rw-r--r--src/routes/(app)/pot/[name]/withdraw/+page.server.js5
-rw-r--r--src/routes/(app)/register/+page.server.js4
-rw-r--r--src/routes/(app)/send/[...text]/+page.server.js5
-rw-r--r--src/routes/(app)/send/[...text]/+page.svelte3
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js22
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte110
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.server.js9
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.svelte1
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/+page.server.js14
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/+page.svelte6
-rw-r--r--src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js2
-rw-r--r--src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte2
-rw-r--r--src/routes/(app)/send/user/[username]/[...amount]/+page.server.js10
-rw-r--r--src/routes/(app)/send/user/[username]/[...amount]/+page.svelte6
-rw-r--r--src/routes/(app)/sent/+page.svelte26
-rw-r--r--src/routes/(app)/support/+page.server.js16
-rw-r--r--src/routes/(app)/support/+page.svelte57
-rw-r--r--src/routes/(app)/support/+server.js9
-rw-r--r--src/routes/(static)/+layout.server.js16
-rw-r--r--src/routes/(static)/+layout.svelte6
-rw-r--r--src/routes/(static)/bounties/+page.svelte38
-rw-r--r--src/routes/(static)/docs/+page.server.js4
-rw-r--r--src/routes/(static)/docs/+page.svelte92
-rw-r--r--src/routes/+page.server.js2
-rw-r--r--static/manifest.webmanifest6
-rw-r--r--static/tickets/01.pngbin328175 -> 0 bytes
-rw-r--r--static/tickets/02.pngbin328809 -> 0 bytes
-rw-r--r--static/tickets/03.pngbin328778 -> 0 bytes
-rw-r--r--static/tickets/04.pngbin328539 -> 0 bytes
-rw-r--r--static/tickets/05.pngbin328735 -> 0 bytes
-rw-r--r--static/tickets/06.pngbin328887 -> 0 bytes
-rw-r--r--static/tickets/07.pngbin328562 -> 0 bytes
-rw-r--r--static/tickets/08.pngbin329032 -> 0 bytes
-rw-r--r--static/tickets/09.pngbin328984 -> 0 bytes
-rw-r--r--static/tickets/10.pngbin328193 -> 0 bytes
-rw-r--r--static/tickets/11.pngbin327215 -> 0 bytes
-rw-r--r--static/tickets/12.pngbin327960 -> 0 bytes
-rw-r--r--static/tickets/13.pngbin328043 -> 0 bytes
-rw-r--r--static/tickets/14.pngbin327772 -> 0 bytes
-rw-r--r--static/tickets/15.pngbin327942 -> 0 bytes
-rw-r--r--static/tickets/16.pngbin328184 -> 0 bytes
-rw-r--r--static/tickets/17.pngbin327705 -> 0 bytes
-rw-r--r--static/tickets/18.pngbin328330 -> 0 bytes
-rw-r--r--static/tickets/19.pngbin328094 -> 0 bytes
-rw-r--r--static/tickets/20.pngbin328762 -> 0 bytes
-rw-r--r--static/tickets/21.pngbin327907 -> 0 bytes
-rw-r--r--static/tickets/22.pngbin327742 -> 0 bytes
-rw-r--r--static/tickets/23.pngbin328659 -> 0 bytes
-rw-r--r--static/tickets/24.pngbin328342 -> 0 bytes
-rw-r--r--static/tickets/25.pngbin328559 -> 0 bytes
-rw-r--r--static/tickets/26.pngbin328749 -> 0 bytes
-rw-r--r--static/tickets/27.pngbin328339 -> 0 bytes
-rw-r--r--static/tickets/28.pngbin328867 -> 0 bytes
-rw-r--r--static/tickets/29.pngbin328698 -> 0 bytes
-rw-r--r--static/tickets/30.pngbin328710 -> 0 bytes
-rw-r--r--static/tickets/31.pngbin327985 -> 0 bytes
-rw-r--r--static/tickets/32.pngbin328621 -> 0 bytes
-rw-r--r--static/tickets/33.pngbin327720 -> 0 bytes
-rw-r--r--static/tickets/34.pngbin328366 -> 0 bytes
-rw-r--r--static/tickets/35.pngbin328507 -> 0 bytes
-rw-r--r--static/tickets/36.pngbin328767 -> 0 bytes
-rw-r--r--static/tickets/37.pngbin328358 -> 0 bytes
-rw-r--r--static/tickets/38.pngbin328839 -> 0 bytes
-rw-r--r--static/tickets/39.pngbin328764 -> 0 bytes
-rw-r--r--static/tickets/40.pngbin328562 -> 0 bytes
-rw-r--r--static/tickets/41.pngbin327591 -> 0 bytes
-rw-r--r--static/tickets/42.pngbin328279 -> 0 bytes
-rw-r--r--static/tickets/43.pngbin328364 -> 0 bytes
-rw-r--r--static/tickets/44.pngbin327599 -> 0 bytes
-rw-r--r--static/tickets/45.pngbin328331 -> 0 bytes
-rw-r--r--static/tickets/46.pngbin328518 -> 0 bytes
-rw-r--r--static/tickets/47.pngbin328120 -> 0 bytes
-rw-r--r--static/tickets/48.pngbin328703 -> 0 bytes
-rw-r--r--static/tickets/49.pngbin328488 -> 0 bytes
-rw-r--r--static/tickets/50.pngbin328729 -> 0 bytes
-rw-r--r--tailwind.config.cjs3
126 files changed, 1790 insertions, 759 deletions
diff --git a/package.json b/package.json
index 908ec0bf..0ba0e7b7 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"leaflet": "^1.9.3",
"leaflet.markercluster": "^1.5.3",
"nostr": "^0.2.7",
+ "nostr-tools": "^1.7.4",
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"prettier-plugin-svelte": "^2.9.0",
@@ -36,6 +37,7 @@
"svelte-preprocess": "^4.10.7",
"svelte-virtual-scroll-list": "^1.1.0",
"sveltekit-i18n": "^2.2.2",
+ "tailwind-scrollbar": "^2.1.0",
"tailwindcss": "^3.2.4",
"tiny-secp256k1": "^2.2.1",
"uuid": "^9.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4f0d2ef2..48eb168b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,6 +16,7 @@ specifiers:
leaflet: ^1.9.3
leaflet.markercluster: ^1.5.3
nostr: ^0.2.7
+ nostr-tools: ^1.7.4
postcss: ^8.4.20
prettier: ^2.8.1
prettier-plugin-svelte: ^2.9.0
@@ -29,6 +30,7 @@ specifiers:
svelte-preprocess: ^4.10.7
svelte-virtual-scroll-list: ^1.1.0
sveltekit-i18n: ^2.2.2
+ tailwind-scrollbar: ^2.1.0
tailwindcss: ^3.2.4
tiny-secp256k1: ^2.2.1
uuid: ^9.0.0
@@ -55,6 +57,7 @@ devDependencies:
leaflet: 1.9.3
leaflet.markercluster: [email protected]
nostr: 0.2.7
+ nostr-tools: 1.7.4
postcss: 8.4.20
prettier: 2.8.1
prettier-plugin-svelte: 2.9.0_ajxj753sv7dbwexjherrch25ta
@@ -68,6 +71,7 @@ devDependencies:
svelte-preprocess: 4.10.7_qs7rgzvahok4cbkjoy5bgypde4
svelte-virtual-scroll-list: [email protected]
sveltekit-i18n: [email protected]
+ tailwind-scrollbar: [email protected]
tailwindcss: [email protected]
tiny-secp256k1: 2.2.1
uuid: 9.0.0
@@ -276,6 +280,18 @@ packages:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: false
+ /@noble/hashes/1.0.0:
+ resolution: {integrity: sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg==}
+ dev: true
+
+ /@noble/hashes/1.2.0:
+ resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==}
+ dev: true
+
+ /@noble/secp256k1/1.7.1:
+ resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==}
+ dev: true
+
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -388,6 +404,25 @@ packages:
rollup: 3.8.1
dev: false
+ /@scure/base/1.1.1:
+ resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
+ dev: true
+
+ /@scure/bip32/1.1.5:
+ resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==}
+ dependencies:
+ '@noble/hashes': 1.2.0
+ '@noble/secp256k1': 1.7.1
+ '@scure/base': 1.1.1
+ dev: true
+
+ /@scure/bip39/1.1.1:
+ resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==}
+ dependencies:
+ '@noble/hashes': 1.2.0
+ '@scure/base': 1.1.1
+ dev: true
+
/@sveltejs/adapter-node/1.1.4_@[email protected]:
resolution: {integrity: sha512-3iEBqi1fXLXP9YIbVuz2LXajoebRJCmAFEQbN40DlxAnA7G+InxUgnqFun3q9gBMz2Qvd99K51g/HxWetXRe8Q==}
peerDependencies:
@@ -1899,6 +1934,17 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /nostr-tools/1.7.4:
+ resolution: {integrity: sha512-YowDJ+S3UW9KCYPDZfZXXMITrJSMjiCmFOK5HohyKkg+w6EipFUTkFRBPRA2BTLXO/qw8gukKXfL0B7Dv3jtcQ==}
+ dependencies:
+ '@noble/hashes': 1.0.0
+ '@noble/secp256k1': 1.7.1
+ '@scure/base': 1.1.1
+ '@scure/bip32': 1.1.5
+ '@scure/bip39': 1.1.1
+ prettier: 2.8.4
+ dev: true
+
/nostr/0.2.7:
resolution: {integrity: sha512-Yq5tkiCTJtohs7H7Fx+Ki83bR8b3xapdwYi17HunhW5EQfZ9rI0cIXFyihj2ZRkv/0YYvVBiCp/KIaLYt5LyfQ==}
dependencies:
@@ -2100,6 +2146,12 @@ packages:
hasBin: true
dev: true
+ /prettier/2.8.4:
+ resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+ dev: true
+
/process/0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
@@ -2477,6 +2529,15 @@ packages:
svelte: 3.55.0
dev: true
+ /tailwind-scrollbar/[email protected]:
+ resolution: {integrity: sha512-zpvY5mDs0130YzYjZKBiDaw32rygxk5RyJ4KmeHjGnwkvbjm/PszON1m4Bbt2DkMRIXlXsfNevykAESgURN4KA==}
+ engines: {node: '>=12.13.0'}
+ peerDependencies:
+ tailwindcss: 3.x
+ dependencies:
+ tailwindcss: [email protected]
+ dev: true
+
/tailwindcss/[email protected]:
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
engines: {node: '>=12.13.0'}
diff --git a/src/app.css b/src/app.css
index 12fad732..3a722fb1 100644
--- a/src/app.css
+++ b/src/app.css
@@ -35,7 +35,6 @@ button.primary {
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
- background-image: url('/icons/down.svg');
background-repeat: no-repeat;
background-position-x: 95%;
background-position-y: 1em;
diff --git a/src/components/About.svelte b/src/components/About.svelte
index d51b9268..0a0ff874 100644
--- a/src/components/About.svelte
+++ b/src/components/About.svelte
@@ -18,19 +18,6 @@
socials: []
},
{
- name: 'karnage',
- socials: [
- { type: 'twitter', url: 'https://twitter.com/KarnageBitcoin', color: 'bg-[#1D9BF0]' }
- ]
- },
- {
- name: 'secondl1ght',
- socials: [
- { type: 'twitter', url: 'https://www.twitter.com/secondl1ght', color: 'bg-[#1D9BF0]' },
- { type: 'github', url: 'https://github.com/secondl1ght', color: 'bg-gray-800' }
- ]
- },
- {
name: 'shauna',
socials: [
{ type: 'twitter', url: 'https://twitter.com/ShaunaCollister', color: 'bg-[#1D9BF0]' },
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index 74afd7b9..51c70851 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -15,7 +15,6 @@
let menuButtons;
if (user)
menuButtons = [
- { key: 'nav.profile', icon: 'profile', href: `/${user.username}` },
{ key: 'nav.settings', icon: 'settings', href: `/${user.username}/settings` },
{ key: 'nav.support', icon: 'support', href: `/support` },
{ key: 'nav.signOut', icon: 'logout', href: `/logout` }
diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte
index e95e5dde..cc923b7d 100644
--- a/src/components/Balance.svelte
+++ b/src/components/Balance.svelte
@@ -9,7 +9,7 @@
<div class="space-y-2">
<div class="text-5xl font-bold tabular-nums">
{#if isNaN($animatedRate)}
- <div class="text-gray-200">---</div>
+ <div class="text-gray-200">&mdash;</div>
{:else}
{f((user.balance * $animatedRate) / sats, user.currency)}
{/if}
diff --git a/src/components/Code.svelte b/src/components/Code.svelte
new file mode 100644
index 00000000..0e09191e
--- /dev/null
+++ b/src/components/Code.svelte
@@ -0,0 +1,31 @@
+<script>
+ import { copy } from '$lib/utils';
+ import { Icon } from '$comp';
+ import samples from '$lib/samples';
+ export let sample;
+</script>
+
+<div
+ class="bg-black text-white rounded-lg p-4 flex gap-4 whitespace-pre-wrap font-mono"
+>
+ <div class="overflow-x-scroll max-w-full scrollbar-hide">
+ <div class="break-all min-w-[800px]">{samples[sample]}</div>
+ </div>
+ <button
+ class="ml-auto my-auto invert opacity-90"
+ on:click={() => copy(samples[sample].replace(/^\s+/gm, '').replace(/\n/g, ''))}
+ ><Icon icon="copy" style="w-10 max-w-none" /></button
+ >
+</div>
+
+<style>
+ .scrollbar-hide::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* For IE, Edge and Firefox */
+ .scrollbar-hide {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+ }
+</style>
diff --git a/src/components/LandingHeader.svelte b/src/components/LandingHeader.svelte
index f9db5ea0..ed7907c1 100644
--- a/src/components/LandingHeader.svelte
+++ b/src/components/LandingHeader.svelte
@@ -40,29 +40,29 @@
<button class="hover:opacity-80" on:click={() => scroll(about)}>{$t('about.header')}</button
>
{/if}
- {#if !user}
+ {#if user}
<button
- class="border rounded-full px-6 py-2 font-bold hover:opacity-80"
- on:click={() => goto('/register')}
- >{$t('nav.startInSeconds')}
+ class="border rounded-full px-6 py-2 font-bold"
+ on:click={() => goto(`/${user.username}`)}
+ >Home
</button>
<button
- class="bg-black text-white border rounded-full px-6 py-2 font-bold hover:opacity-80"
- on:click={() => goto('/login')}
+ class="bg-black text-white border rounded-full px-6 py-2 font-bold"
+ on:click={() => goto('/logout')}
>
- {$t('nav.signIn')}
+ {$t('nav.signOut')}
</button>
{:else}
<button
- class="border rounded-full px-6 py-2 font-bold"
- on:click={() => goto(`/${user.username}`)}
- >{$t('nav.account')}
+ class="border rounded-full px-6 py-2 font-bold hover:opacity-80"
+ on:click={() => goto('/register')}
+ >{$t('nav.startInSeconds')}
</button>
<button
- class="bg-black text-white border rounded-full px-6 py-2 font-bold"
- on:click={() => goto('/logout')}
+ class="bg-black text-white border rounded-full px-6 py-2 font-bold hover:opacity-80"
+ on:click={() => goto('/login')}
>
- {$t('nav.signOut')}
+ {$t('nav.signIn')}
</button>
{/if}
</div>
diff --git a/src/components/Lnurlp.svelte b/src/components/Lnurlp.svelte
index 22364fcf..ab0c50ce 100644
--- a/src/components/Lnurlp.svelte
+++ b/src/components/Lnurlp.svelte
@@ -8,7 +8,7 @@
let { currency } = data.user;
let { minSendable, maxSendable, callback, metadata } = data;
- let amount = minSendable,
+ let amount = Math.round(minSendable / 1000),
loading;
let submit = () => (loading = true);
</script>
diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte
index 5ffaa2ea..47bd9375 100644
--- a/src/components/Numpad.svelte
+++ b/src/components/Numpad.svelte
@@ -9,11 +9,18 @@
fiat = !amount,
submit = undefined;
- export let amountFiat = amount ? amount * ($selectedRate / sats) : 0;
-
+ let amountFiat = 0;
let arrow = '<';
- let amountSats = amount;
+ $: update(amount, amountFiat);
+ let update = (a, f) => {
+ if (fiat) {
+ amount = Math.round(f / ($selectedRate / sats));
+ } else {
+ amountFiat = ((a * $selectedRate) / sats).toFixed(2);
+ }
+ };
+
let loading = false;
let symbol =
@@ -33,14 +40,6 @@
KRW: '₩'
}[currency] || '';
- $: amount = parseInt(fiat ? Math.round(amountFiat / ($selectedRate / sats)) : amountSats);
-
- $: amountSatsFormatted = s(amountSats);
-
- $: amountFiatConverted = f(amountSats * ($selectedRate / sats), currency);
-
- $: amountSatsConverted = sat(amountFiat / ($selectedRate / sats));
-
const numPad = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', '<'];
const handleInput = (value) => {
@@ -58,7 +57,7 @@
amountFiat = amountFiat.slice(0, amountFiat.length - 1);
if (amountFiat.length === 0) {
amountFiat = 0;
- amountSats = 0;
+ amount = 0;
}
}
} else if (value !== '.' && value !== '<' && parseInt(amountFiat + value) > $selectedRate) {
@@ -71,25 +70,27 @@
} else {
if (value === '.') {
return;
- } else if (!amountSats && value !== '<' && value !== '0') {
- amountSats = parseInt(value);
+ } else if (!amount && value !== '<' && value !== '0') {
+ amount = parseInt(value);
} else if (value === '<') {
- if (amountSats !== 0) {
- amountSats = Math.floor(amountSats / 10);
- if (amountSats.length === 0) {
- amountSats = 0;
+ if (amount !== 0) {
+ amount = Math.floor(amount / 10);
+ if (amount.length === 0) {
+ amount = 0;
amountFiat = 0;
}
}
- } else if (value !== '<' && parseInt(amountSats + value) > sats) {
+ } else if (value !== '<' && parseInt(amount + value) > sats) {
warning($t('user.receive.lessThan1BTCWarning'));
} else {
- amountSats = parseInt(amountSats + value);
+ console.log('CLICK', amount.toString(), value);
+ amount = parseInt(amount.toString() + value);
+ console.log('AMOUNT', amount);
}
}
};
- $: html = fiat ? amountFiat : s(amountSats);
+ $: html = fiat ? amountFiat : s(amount);
let prev = '';
@@ -124,10 +125,10 @@
if (fiat) {
amountFiat = html;
} else {
- amountSats = parseInt(html.replace(/,/g, ''));
+ amount = parseInt(html.replace(/,/g, ''));
}
- if (!amountSats) amountSats = null;
+ if (!amount) amount = null;
setTimeout(() => {
if (!fiat) {
@@ -178,16 +179,16 @@
/>
</div>
<div class="mt-2">
- <span class="text-secondary mr-1">{fiat ? amountSatsConverted : amountFiatConverted}</span>
+ <span class="text-secondary mr-1">{fiat ? sat(amount) : f(amountFiat, currency)}</span>
<button
type="button"
on:click={() => {
if (fiat) {
- amountSats = parseInt((amountFiat / ($selectedRate / sats)).toFixed(0));
+ amount = parseInt((amountFiat / ($selectedRate / sats)).toFixed(0));
} else {
amountFiat =
- (amountSats * ($selectedRate / sats)).toFixed(2) > 0.0
- ? (amountSats * ($selectedRate / sats)).toFixed(2)
+ (amount * ($selectedRate / sats)).toFixed(2) > 0.0
+ ? (amount * ($selectedRate / sats)).toFixed(2)
: 0;
}
fiat = !fiat;
diff --git a/src/components/Slider.svelte b/src/components/Slider.svelte
new file mode 100644
index 00000000..3c94d127
--- /dev/null
+++ b/src/components/Slider.svelte
@@ -0,0 +1,56 @@
+<script>
+ export let value,
+ handle,
+ min = 0,
+ max = 100;
+</script>
+
+<input
+ id="slider"
+ type="range"
+ {min}
+ {max}
+ bind:value
+ class="px-0 py-0 my-5"
+ style="--bgPercent: {((value - min) * 100) / (max - min)}%;"
+ on:input={handle}
+/>
+
+<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;
+ }
+</style>
diff --git a/src/components/index.js b/src/components/index.js
index ab54e09f..1853e7c9 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,3 +1,5 @@
+export { default as Code } from './Code.svelte';
+export { default as Slider } from './Slider.svelte';
export { default as Pinpad } from './Pinpad.svelte';
export { default as Balance } from './Balance.svelte';
export { default as Lnurlp } from './Lnurlp.svelte';
diff --git a/src/lib/invoice.js b/src/lib/invoice.js
index bb75d4cb..23ce1757 100644
--- a/src/lib/invoice.js
+++ b/src/lib/invoice.js
@@ -11,7 +11,7 @@ export default async ({ cookies, request, url }) => {
let invoice = {
amount,
- tip: parseInt(form.get('tip')),
+ tip: parseInt(form.get('tip')) || 0,
type: form.get('type'),
prompt: form.get('prompt') === 'true',
rate: parseFloat(form.get('rate')) || rates[form.get('currency')],
diff --git a/src/lib/nostr.js b/src/lib/nostr.js
index de19b477..d72f48ef 100644
--- a/src/lib/nostr.js
+++ b/src/lib/nostr.js
@@ -5,7 +5,7 @@ import { Buffer } from 'buffer';
import { bech32m } from 'bech32';
import { bip32, fail, wait, post, stretch } from '$lib/utils';
import { calculateId, signId } from 'nostr';
-import { password as pw, passwordPrompt, pin, loginRedirect } from '$lib/store';
+import { decrypted, password as pw, passwordPrompt, pin, loginRedirect } from '$lib/store';
import { goto, invalidate } from '$app/navigation';
const { encode, decode, fromWords, toWords } = bech32m;
@@ -58,8 +58,10 @@ export let encrypt = async ({ message, recipient, user }) => {
};
export let decrypt = async ({ event, user }) => {
+ let cache = get(decrypted);
try {
- let { content, pubkey } = event;
+ let { content, pubkey, id } = event;
+ if (cache[id]) return cache[id];
if (pubkey === user.pubkey) pubkey = event.tags[0][1];
pubkey = Uint8Array.from(Buffer.from('02' + pubkey, 'hex'));
@@ -78,9 +80,12 @@ export let decrypt = async ({ event, user }) => {
let message = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, cipher);
- return Buffer.from(message).toString('utf8');
+ cache[id] = Buffer.from(message).toString('utf8');
+ decrypted.set(cache);
+
+ return cache[id];
} catch (e) {
- console.log(e);
+ // console.log(e);
}
};
@@ -119,6 +124,7 @@ let getPrivateKey = async (user) => {
seed = mnemonicToSeedSync(mnemonic);
key = bip32.fromSeed(seed);
child = key.derivePath("m/44'/1237'/0'/0/0");
+
return child.privateKey;
};
diff --git a/src/lib/parse.js b/src/lib/parse.js
deleted file mode 100644
index e69de29b..00000000
--- a/src/lib/parse.js
+++ /dev/null
diff --git a/src/lib/samples.js b/src/lib/samples.js
new file mode 100644
index 00000000..93b9aa24
--- /dev/null
+++ b/src/lib/samples.js
@@ -0,0 +1,23 @@
+export default {
+ register: `curl "https://coinos.io/api/register" -H "application/json" -d '{
+ "username": "adam",
+ "password": "hunter2"
+}'`,
+ login: `curl "https://coinos.io/api/login" -H "application/json" -d '{
+ "username": "adam",
+ "password": "hunter2"
+}'`,
+ invoice: `curl "https://coinos.io/api/invoice"
+ -H "application/json"
+ -H "Authorization: Bearer $token"
+ -d '{
+ "invoice": {
+ "amount": 1000,
+ "type": "lightning",
+ "webhook": "https://example.com/payment/received",
+ "secret": "hunter2"
+ },
+}'`,
+ fetchInvoice: `curl "https://coinos.io/api/invoice/bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj"
+ -H "application/json"'`
+};
diff --git a/src/lib/socket.js b/src/lib/socket.js
index ce3cbe4b..aef8a513 100644
--- a/src/lib/socket.js
+++ b/src/lib/socket.js
@@ -1,23 +1,11 @@
import { get } from 'svelte/store';
-import {
- events as $events,
- invoice,
- request,
- newPayment,
- last,
- rate,
- payments,
- user
-} from '$lib/store';
+import { event, invoice, request, newPayment, last, rate, payments, user } from '$lib/store';
import { success, sat } from '$lib/utils';
import { PUBLIC_SOCKET } from '$env/static/public';
import { invalidate } from '$app/navigation';
import { browser } from '$app/environment';
let socket, token;
-let le = browser && localStorage.getItem('events');
-let events = le && le !== 'undefined' ? JSON.parse(le) : {};
-let q = Object.keys(events);
export const auth = () => token && send('login', token) && send('heartbeat');
@@ -30,23 +18,12 @@ export const messages = (data) => ({
id() {
last.set(Date.now());
},
-
invoice() {
invoice.set(data);
},
-
event() {
- data.seen = Math.round(Date.now() / 1000);
- events[data.id] = data;
- q.push(data.id);
- $events.set(events);
- while (q.length > 10000) delete events[q.shift()];
- localStorage.setItem(
- 'events',
- JSON.stringify(q.slice(-100).reduce((a, b) => (a = { ...a, [b]: events[b] }), {}))
- );
+ event.set(data);
},
-
rate() {
rate.set(data);
},
diff --git a/src/lib/store.js b/src/lib/store.js
index e08d9f94..7f43e914 100644
--- a/src/lib/store.js
+++ b/src/lib/store.js
@@ -36,11 +36,12 @@ export const animatedRate = writable(0);
export const avatar = writable();
export const banner = writable();
export const colorTheme = writable('from-primary to-gradient');
+export const event = writable();
export const events = writable({});
export const invoice = writable();
export const last = writable();
export const loginRedirect = writable();
-export const messages = persistLocal('messages', {});
+export const decrypted = persistLocal('decrypted', {});
export const newPayment = persistLocal('newPayment');
export const password = persistLocal('password');
export const passwordPrompt = writable();
diff --git a/src/lib/translations/index.js b/src/lib/translations/index.js
index 9c798530..dfd7f47f 100644
--- a/src/lib/translations/index.js
+++ b/src/lib/translations/index.js
@@ -46,6 +46,11 @@ const config = {
locale: 'de',
key: '',
loader: async () => (await import('../../locales/de.json')).default
+ },
+ {
+ locale: 'zh',
+ key: '',
+ loader: async () => (await import('../../locales/zh.json')).default
}
]
};
diff --git a/src/lib/translations/lang.json b/src/lib/translations/lang.json
index c537d4cf..c607ebd0 100644
--- a/src/lib/translations/lang.json
+++ b/src/lib/translations/lang.json
@@ -3,5 +3,6 @@
"fr": "Français",
"es": "Español",
"de": "Deutsch",
- "pt": "Português"
+ "pt": "Português",
+ "zh": "中国人"
}
diff --git a/src/lib/utils.js b/src/lib/utils.js
index d510195b..6617f76e 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -147,6 +147,14 @@ export const login = async (user, cookies) => {
export const auth = (cookies) => ({ authorization: `Bearer ${cookies.get('token')}` });
+export const fiat = (amount, rate) => (amount * rate) / sats;
+export const fd = async (req) => {
+ let obj = Object.fromEntries(await req.formData());
+ for (let k in obj)
+ (obj[k] === 'undefined' && (obj[k] = undefined)) || (obj[k] === 'false' && (obj[k] = false));
+ return obj;
+};
+
export const f = (s, currency) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
@@ -236,4 +244,12 @@ export const validateEmail = (email) => {
const re =
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
-}; \ No newline at end of file
+};
+
+export const types = {
+ bitcoin: 'bitcoin',
+ lightning: 'lightning',
+ internal: 'internal',
+ pot: 'pot',
+ classic: 'classic'
+};
diff --git a/src/locales/en.json b/src/locales/en.json
index c515432a..350f8a05 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -15,17 +15,6 @@
"position": "Chief Technical Officer",
"bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children."
},
- "secondl1ght": {
- "name": "secondl1ght",
- "position": "Web Developer",
- "bio": "A self-taught Web Developer, secondl1ght dove head first down the bitcoin rabbit hole and left his fiat job to focus on bitcoin development full-time. He contributes to open source projects, is a privacy advocate, and freedom lover."
- },
-
- "karnage": {
- "name": "Karnage",
- "position": "Product Designer",
- "bio": "My mission is to help startup founders succeed. I create products to achieve this goal. Pixel-perfect product design. Get it shipped."
- },
"shauna": {
"name": "Shauna",
"position": "Operations & Marketing",
@@ -264,7 +253,7 @@
"id": "Transaction ID",
"details": "Transaction Details",
"header": "Payments",
- "preimage": "Preimage",
+ "preimage": "Proof of payment",
"type": "Type",
"time": "Time",
"sent": "Sent",
@@ -298,6 +287,6 @@
"redeem": "Redeem",
"redeemed": "Redeemed",
"redeemedBy": "Redeemed by",
- "sendMessage": "Message"
+ "sendMessage": "Write a message..."
}
}
diff --git a/src/locales/es.json b/src/locales/es.json
index fe5ee75f..8e68a5f3 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -1,275 +1,282 @@
{
"404": {
"404": "404",
- "pageNotFound": "La página no existe."
+ "pageNotFound": "Pagina no encontrada."
},
"about": {
- "header": "Sobre",
- "p1": "coinos es una aplicación de código abierto para pagos con bitcoin. Su desarrollo comezó en Vancouver allá por Septiembre de 2012 como una solución para que comerciantes locales puedan aceptar pagos con bitcoin.",
- "p3": "Si lo desea, puede descargar el código fuente de Coinos para ejecutar su propia instancia y tenerla conectada a sus propios nodos bitcoin y lightning. Estaremos encantados de ayudarle a en un proveedor de pagos y en un custodio benevolente para su comunidad.",
- "p4": "Las transferencias entre cuentas de usuario en nuestro servidor son realizadas off-chain, esto hace que sean instantáneas, ilimitadas, y gratuitas.",
+ "header": "Sobre Nosotros",
+ "p1": "Coinos es una aplicación de pagos en bitcoin de código abierto. Este desarrollo comenzó en septiembre de 2012 en Vancouver como una forma de proporcionar a los comerciantes locales una forma conveniente de aceptar pagos con bitcoin.",
+ "p2": "Descarga el código fuente para ejecutar una instancia y conectarla a tu propio nodo bitcoin y lightning. Te ayudaremos a convertirte en un excelente custodio y proveedor de pagos para tu comunidad.",
+ "p3": "Las transferencias de bitcoin entre usuarios en nuestro servidor se realizan off-chain, por lo que son completamente gratuitas, instantáneas e ilimitadas.",
"team": {
- "header": "El Equipo",
+ "header": "Nuestro Equipo",
"adam": {
"name": "Adam Soltys",
- "position": "Director (CTO)",
- "bio": "Adam es desarrollador web y entusiasta de bitcoin, cree que el dinero sólido es clave para aumentar la prosperidad global. Vive en Vancouver junto a su esposa Chelsea y sus dos hijos."
+ "position": "Director de Tecnología",
+ "bio": "Adam es un desarrollador web y un entusiasta de bitcoin que cree que el dinero sólido es clave para aumentar la prosperidad global. Vive en la ciudad de North Vancouver con su esposa Chelsea y sus dos hijos."
},
"secondl1ght": {
"name": "secondl1ght",
- "position": "Desarrollador web",
- "bio": "Es un desarrollador web autodidacta, que se zambulló de cabeza en la madriguera del conejo de bitcoin al dejar su trabajo pagado en fiat para concentrarse en el desarrollo de bitcoin a tiempo completo. Contribuyente en proyectos de código abierto, defensor de la privacidad y amante de la libertad."
+ "position": "Desarrollador Web",
+ "bio": "Desarrollador web autodidacta, se sumergió de cabeza en con el bitcoin y dejó su trabajo fiduciario para concentrarse en el desarrollo de bitcoin a tiempo completo. Contribuye en proyectos de código abierto, es un defensor de la privacidad y un amante de la libertad."
},
"karnage": {
"name": "Karnage",
"position": "Diseñador de Producto",
- "bio": "¨Mi misión es ayudar a los fundadores de startups a tener éxito. Creo productos para lograr este objetivo. Diseño de producto con píxeles perfectos. Recibe lo que buscas.¨"
+ "bio": "Mi misión es ayudar a los fundadores de startups a tener éxito. Creo productos para lograr estos objetivos. Diseño de producto con píxeles perfectos. Haz que te lo envíen."
},
"shauna": {
"name": "Shauna",
- "position": "Marketing & Operaciones",
- "bio": "Gestión Estratégica y de Proyectos. Ella es una todóloga. Siempre encuentra tiempo para reunirse a tomar un café."
+ "position": "Operaciones y Mercadeo",
+ "bio": "Gestión Estratégica y de Proyectos. Sé un poco de todo. Siempre habrá tiempo para compartir un buen café."
},
"cole": {
"name": "Cole",
- "position": "Profesional en Bitcoin",
- "bio": "Como profesional certificado de Bitcoin, Cole es una persona motivada y apasionada por la libertad. Encontró bitcoin en el año 2016 bitcoin y se dedicó a estudiarlo en profundidad. Ahora quiere presentar Lightning Network a tantos usuarios como sea posible."
+ "position": "Profesional de Bitcoin",
+ "bio": "Como profesional certificado de Bitcoin, Cole es una persona motivada y apasionada por la libertad. Conoció el bitcoin en 2016 y lo estudió en profundidad. Ahora quiere presentarle la Red Lightning a tantos usuarios como sea posible."
},
"john": {
"name": "John Barratt",
- "position": "Desarrollador web Full Stack",
- "bio": "Cuando no está programando, John puede ser encontrado en el bosque recorriendo todos los grandes senderos que Colombia Británica tiene para ofrecer."
+ "position": "Desarrollador Web Full Stack",
+ "bio": "Cuando no está programando, podemos encontrar a John en el bosque recorriendo todos los grandes senderos que British Columbia tiene para ofrecer."
}
},
"locations": {
- "header": "Locales",
- "description": "Estos son comercios que aceptan bitcoin con coinos. Si estás utilizando nuestra aplicación, por favor \"<a href=\"mailto:[email protected]\" class=\"underline underline-offset-4 hover:opacity-80\">cuéntenos!</a>"
+ "header": "Locaciones",
+ "description": "Aquí algunas empresas que aceptan bitcoin con Coinos. ¡Esperamos que el tuyo sea el próximo!"
}
},
"error": {
"header": "Error",
- "message": "Por favor inténtelo nuevamente o contacte a nuestro soporte.",
+ "message": "Vuelva a intentarlo o póngase en contacto con el soporte.",
"signIn": "Por favor inicie sesión para continuar."
},
"faq": {
- "header": "PMF",
+ "header": "Preguntas frecuentes",
"cost": {
- "question": "Cuanto cuesta?",
- "answer": "Coinos es gratis. No hay costos de registro ni mantenimiento. Puede enviar y recibir pagos de forma ilimitada por medio de Lightning Network sin costo alguno. Sin embargo, si quieres tus fondos hacia una dirección de bitcoin on-chain existe una tarifa del 0,1%."
+ "question": "¿Cuánto cuesta?",
+ "answer": "Registrar una cuenta y recibir pagos a través de Red Lightning es gratis. Cobramos una tarifa del 0,1% por convertir de lightning a bitcoin."
},
"compatibility": {
- "question": "Necesito un dispositivo o software especial?",
- "answer": "No. Al estar basado en web, Coinos está disponible en cualquier dispositivo con conexión a internet. Puede usarlo directamente desde un celular o una tableta."
+ "question": "¿Necesito algún dispositivo o software especial?",
+ "answer": "No. Coinos es una aplicación web, por lo que siempre que tenga conexión a Internet, podrá usarlo desde cualquier dispositivo. Las tiendas minoristas pueden usarlo desde un teléfono o una tableta."
},
"safety": {
- "question": "Qué puedo hacer con el bitcoin que recibo?",
- "answer": "Usarlo para pagar personal y proveedores, apoyar otras empresas y comerciantes que acepten bitcoin. O puede mantenerlo durante algunos años y ver crecer su poder adquisitivo."
+ "question": "¿Cuándo se liquidan mis fondos?",
+ "answer": "Los pagos en Red Lightning se liquidan al instante. Puede retirar su saldo a otra billetera o exchange inmediatamente después de recibir el pago."
}
},
"footer": {
"company": "Empresa",
- "followUs": "Síganos",
+ "followUs": "Síguenos",
"github": "GitHub",
- "jobs": "Empleos",
+ "jobs": "Trabaje con nosotros",
"support": "Soporte",
"telegram": "Telegram",
"twitter": "Twitter",
"news": "Noticias"
},
"howItWorks": {
- "header": "¿Cómo funciona?",
+ "header": "Cómo funciona",
"step1": {
- "header": "1. Abra una cuenta",
- "description": "Para empezar, solo debe elegir un nombre de usuario y crear una contraseña."
+ "header": "1. Crea tu cuenta",
+ "description": "Un nombre de usuario y una contraseña es todo lo que necesita para comenzar."
},
"step2": {
- "header": "2. Solicitar pagos",
- "description": "Lo mismo que hace normalmente, ¡pero con bitcoin!"
+ "header": "2. Cobra tus pagos",
+ "description": "Lo mismo que harías normalmente, pero con bitcoin."
},
"step3": {
- "header": "3. ¡y listo!",
- "description": "Ahora puede disponer libremente de su bitcoin como guste."
+ "header": "3. ¡Eso es todo!",
+ "description": "Haz pagos con otros comerciantes o guarda tu bitcoin para días difíciles, la elección es tuya."
}
},
"invoice": {
- "addTip": "Agregar propina",
- "addTipq": "Agregar propina?",
- "done": "Listo",
- "paymentSuccessful": "Pago exitoso!",
+ "addTip": "Añadir propina",
+ "addTipq": "Añadir propina?",
+ "done": "Hecho",
+ "paymentSuccessful": "¡Pago recibido!",
"invoice": "Factura",
"total": "Total",
"tip": "Propina"
},
"jobs": {
- "header": "Empleos",
- "subheader": "Puestos disponibles",
- "pitch1": "coinos se encuentra buscando a desarrolladores que quieran unirse a nuestro equipo de trabajo en crecimiento (tiempo medio o tiempo completo, APROBAMOS el trabajo remoto).",
- "pitch2": "Emocionado por trabajar en un proyecto de código abierto con bitcoin? Entonces mira nuestros repositorios en",
+ "header": "Trabaje con nosotros",
+ "subheader": "Ofertas de Trabajo",
+ "pitch1": "Coinos está activamente buscando desarrolladores para unirse a nuestro equipo en crecimiento (tiempo completo o medio tiempo, trabajo remoto OK).",
+ "pitch2": "¿Emocionado por trabajar en bitcoin de código abierto? Entonces eche un vistazo a nuestros repositorios en",
"github": "GitHub",
- "pitch3": "Nos encantaría saber de usted si cree que tiene algo impactante para mostrar. Envíenos su CV a"
+ "pitch3": "Si crees que puedes generar impacto, nos encantaría saber de ti. Envíanos tu currículum a"
},
"landing": {
- "header_before_icon": "La manera",
- "header_with_icon": "más simple",
- "header_after_icon": "de aceptar bitcoin en su negocio.",
- "subheader": "Aumente sus posibilidades de ingreso agregando bitcoin como forma de pago mediante una aplicación web muy fácil de usar. No se requiere ningún software o hardware adicional.",
+ "header_before_icon": "La",
+ "header_with_icon": "manera",
+ "header_after_icon": "más fácil de aceptar bitcoins en tu tienda.",
+ "subheader": "Aumenta tu número de clientes e ingresos al aceptar bitcoin con una aplicación web fácil de usar. No requiere software ni hardware.",
"info1": {
- "title": "Pagos instantáneos con Lightning",
- "description": "Cuando su cliente envíe un pago, el mismo estará disponible en forma inmediata. Liquidación instantánea en segundos. Diga adiós al 3% de interés que le cobran por tarifas de procesamiento de tarjetas de crédito."
+ "title": "Pagos instantáneos a través de Lightning",
+ "description": "El momento en que su cliente envía un pago, este llega inmediatamente. Sin esperar liquidaciones bancarias ni preocuparse por cargos extras. Liquidación instantánea, en segundos. Diga adiós a los cargos del 3% de las tarifas en tarjetas de crédito."
},
"info2": {
"title": "Fácil de usar",
- "description1": "Ingrese un monto en su moneda local, comparta el QR y listo.",
- "description2": "Coinos le ofrece acceso gratuito para usar los nodos bitcoin y lightning alojados, sin que deba comprender cómo funcionan bitcoin o lightning. Hacemos todo el trabajo pesado para que pueda concentrarse en su negocio.",
- "description3": "Usted siempre tendrá la opción de retirar sus fondos para retomar su custodia de forma inmediata."
+ "description1": "Solo ingrese una cantidad en su moneda local, comparta el código QR y listo.",
+ "description2": "Hacemos todo el trabajo pesado para que pueda concentrarse en su negocio.",
+ "description3": "Siempre tiene la opción de tomar la custodia de sus fondos al retirarlos instantáneamente en cualquier momento."
},
"info3": {
- "title": "Personalícelo!",
- "description": "Puede personalizar la apariencia y el estilo de su punto de venta o mantenerlo por defecto. Depende de usted."
+ "title": "Maneja tu marca a tu manera!",
+ "description": "Personalice la apariencia y el estilo del punto de venta de tu negocio a tu gusto: depende totalmente de ti."
}
},
"login": {
"optional": "Opcional",
"token": "Token 2FA",
- "username": "Nombre de usuario",
+ "username": "Usuario",
"password": "Contraseña",
- "email": "Email",
- "passwordRecommendation": "Recomendamos utilizar un administrador de contraseñas para generar una clave larga y aleatoria.",
- "forgotUserOrPassword": "Olvidó su nombre de usuario o su contraseña?",
- "haveAccount": "Ya tiene una cuenta?",
- "noAccount": "No tiene una cuenta?",
- "register": "Registrarse",
- "signIn": "Iniciar sesión",
- "failed": "Inicio de sesión fallido"
+ "email": "Correo",
+ "passwordRecommendation": "Recomendamos una contraseña larga y aleatoria generada con un administrador de contraseñas.",
+ "forgotUserOrPassword": "¿Olvidaste tu nombre de usuario o contraseña?",
+ "haveAccount": "¿Ya tienes una cuenta?",
+ "noAccount": "¿No tienes una cuenta?",
+ "register": "Registro",
+ "signIn": "Registrarse",
+ "failed": "Fallo en Inicio"
},
"nav": {
"profile": "Perfil",
- "startInSeconds": "Comience de inmediato",
+ "startInSeconds": "Comience en segundos",
"account": "Cuenta",
- "signIn": "Iniciar sesión",
- "signOut": "Cerrar sesión",
+ "signIn": "Registrarse",
+ "signOut": "Desconectar",
"dashboard": "Panel",
"settings": "Ajustes",
"support": "Soporte"
},
+ "newSiteHeader": "Acompañanos en nuestra <a href=\"/launch\" class=\"underline underline-offset-4 text-black hover:opacity-80\">launch party</a> en Vancouver el 26 de Octubre! Buscando la version anterior de coinos? Visita <a href=\"https://classic.coinos.io\" class=\"underline underline-offset-4 text-black hover:opacity-80\">classic.coinos.io</a>",
"news": {
"header": "Noticias",
- "subheader": "Últimas novedades",
+ "subheader": "Últimas Actualizaciones",
"update1": ""
},
"user": {
"bitcoin": "Bitcoin",
- "lightning": "Relámpago",
+ "lightning": "Lightning",
"invoice": "Factura",
"unfollow": "Dejar de seguir",
"following": "Siguiendo",
"follow": "Seguir",
"remove": "Eliminar",
- "scanToPayBTC": "Escanea para pagar en bitcoin",
+ "scanToPayBTC": "Escanear para pagar con bitcoin",
"this": "Esta",
- "lightningAddress": "dirección relámpago",
- "likeAnEmail": "es como un correo electrónico para bitcoin. Puede arlo en su sitio web o tarjetas de presentación y usarlo varias veces.",
+ "lightningAddress": "dirección lightning",
+ "likeAnEmail": "es como un correo electrónico para bitcoin. Puede publicarlo en su sitio web o tarjetas de presentación y usarlo múltiples veces.",
"dashboard": {
- "dashboard": "Tablero",
- "accountBalance": "Saldo",
- "bitcoinPrice": "Precio de bitcoin:",
- "conversionFeeWarning": "Cobraremos una tarifa de retiro del 1% sobre el monto",
+ "go": "Ir",
+ "dashboard": "Panel",
+ "accountBalance": "Saldo de la cuenta",
+ "bitcoinPrice": "Precio de Bitcoin:",
+ "conversionFeeWarning": "Se le cobrará una tarifa de retiro del 1% sobre el monto.",
"dataFromBinance": "Datos de <span class=\"text-black\">Binance</span>.",
- "fetchingRate": "Tasa...",
+ "fetchingRate": "calculando tasa...",
"optionalNote": "Nota (Opcional)",
"send": "Enviar",
- "receive": "Recibir un pago",
+ "receive": "Recibir",
"withdraw": "Retirar",
- "deposit": "Depósito",
- "voucher": "Vale"
+ "deposit": "Depositar",
+ "voucher": "Comprobante"
},
"receive": {
- "lessThan1BTCWarning": "Ingrese una cantidad menor a 1 bitcoin.",
- "showQR": "Ver QR"
+ "lessThan1BTCWarning": "Introduzca una cantidad inferior a 1 bitcoin.",
+ "showQR": "Mostrar código QR"
},
"send": {
"contacts": "Contactos",
"scan": "Escanear",
"paste": "Pegar",
- "next": "Próxima",
- "placeholder": "Pegue una dirección de bitcoin, una factura relámpago o un nombre de usuario de coinos"
+ "next": "Siguiente",
+ "placeholder": "Asigne una dirección de bitcoin, una factura lightning o un usuario de coinos"
},
"settings": {
- "bannerImage": "Imagen de portada",
+ "bannerImage": "Imagen del Banner",
"profileImage": "Imagen de perfil",
"oneTimeCode": "Ingrese el código de un solo uso",
- "accountId": "",
+ "accountId": "Id Cuenta",
"header": "Ajustes",
"ACCOUNT": "CUENTA",
"POINT_OF_SALE": "PUNTO DE VENTA",
"SECURITY": "SEGURIDAD",
- "locale": "Languaje",
- "username": "Nombre de usuario",
- "email": "Email",
+ "locale": "Idioma",
+ "username": "Usuario",
+ "email": "Correo",
"newPassword": "Nueva contraseña",
- "businessAddress": "Dirección comercial",
- "emailNotifications": "Notificar por email",
- "emailNotificationsDescription": "Recibe notificaciones cuando alguien le envíe un pago o cuando ocurra algún cambio en su cuenta.",
- "smsNotifications": "Notificar pr SMS",
- "smsNotificationsDescription": "coinos le enviará notificaciones por mensaje de texto cuando esté habilitado.",
- "theme": "Tema de color",
- "themeDescription": "Elija el tema de color predeterminado.",
- "localCurrency": "Moneda local",
- "btcUnit": "Mostrar bitcoin en:",
- "bitcoin": "Bitcoin",
+ "businessAddress": "Dirección Negocio",
+ "emailNotifications": "Notificaciones de Correo Electrónico",
+ "emailNotificationsDescription": "Reciba notificaciones por correo electrónico cuando se produzca un cambio en su cuenta o cuando las personas le envíen pagos.",
+ "smsNotifications": "Notificaciones SMS",
+ "smsNotificationsDescription": "Coinos enviará notificaciones de texto cuando esto esté habilitado.",
+ "theme": "Color Tema",
+ "themeDescription": "Seleccione entre los temas de color predeterminados.",
+ "localCurrency": "Moneda Local",
+ "btcUnit": "Ver bitcoin en:",
+ "bitcoin": "bitcoin",
"satoshis": "Satoshis",
- "securityPIN": "PIN de seguridad",
- "securityPINDescription": "Ingrese un pin de seguridad y úselo para desbloquear coinos o enviar pagos.",
- "setPIN": "Crear PIN",
- "autoLock": "Tiempo de auto bloqueo",
- "autoLockDescription": "Se le pedirá el pin para reanudar la sesión luego de el tiempo especificado:",
+ "securityPIN": "PIN de Seguridad",
+ "securityPINDescription": "Establezca un pin de seguridad para desbloquear la configuración y enviar pagos.",
+ "setPIN": "Establecer PIN",
+ "autoLock": "Temporizador de bloqueo automático",
+ "autoLockDescription": "Su cuenta entrará en modo seguro automáticamente después del tiempo especificado:",
"twofa": "Autenticación de 2 factores",
- "twofaDescription": "Agregue un nivel extra de seguridad a su cuenta con la autenticación de 2 factores (2FA).",
+ "twofaDescription": "Agregue una capa extra de seguridad a su dispositivo habilitando la autenticación de 2 factores.",
"twofaSetup": "Configurar 2FA",
- "twofaDisable": "Desactivar 2FA",
- "select": "Seleccione",
+ "twofaDisable": "Deshabilitar 2FA",
+ "select": "Seleccionar",
"verifyPIN": "Verificar PIN",
"enablePIN": "Habilitar PIN",
"disablePIN": "Deshabilitar PIN"
},
"support": {
"header": "Soporte",
- "email": "Correo electrónico",
- "accountName": "Nombre de cuenta",
+ "email": "Correo",
+ "accountName": "Nombre Cuenta",
"message": "Mensaje",
"send": "Enviar"
}
},
- "payments": {
- "header": "Historial de transacciones",
- "AMOUNT": "MONTO",
+ "transactions": {
+ "header": "Transacciones",
+ "AMOUNT": "CANTIDAD",
"TYPE": "TIPO",
"TIME": "HORA",
- "sent": "Enviada",
- "received": "Recibida",
- "empty": "No hay transacciones.",
- "send": "Enviar",
- "next": "Próxima",
- "sendersChoice": "Elección del remitente",
+ "sent": "Enviado",
+ "received": "Recibido",
+ "empty": "Aún no hay transacciones.",
+ "send": "Enviado",
+ "next": "Siguiente",
+ "sendersChoice": "Sender's choice",
"sendTo": "Enviar a",
- "to": "a",
+ "to": "para",
"pay": "Pagar",
"requesting": "está solicitando",
- "sentInvoiceTo": "Factura enviada a",
- "sentRequestTo": "Solicitud enviada a",
+ "sentInvoiceTo": "Enviar factura a",
+ "sentRequestTo": "Enviar solicitud a",
"pleasePay": "Por favor pague",
- "day": "Dia",
+ "day": "Día",
"week": "Semana",
"month": "Mes",
"all": "Todo",
"export": "Exportar",
"invoice": "Factura",
"readyToPay": "está listo para pagar",
- "describeOrder": "Utilice este campo para describir su pedido",
- "requestAnInvoice": "Solicitar una factura",
+ "describeOrder": "Utilice este campo para describir su orden",
+ "requestAnInvoice": "Solicitar factura",
"submit": "Enviar",
"isRequesting": "está solicitando",
- "awaiting": "Esperando"
+ "awaiting": "Esperando",
+ "tapAnywhere": "Toca en cualquier lugar",
+ "from": "de",
+ "redeem": "Redimir",
+ "redeemed": "Redimido",
+ "redeemedBy": "Redimido por"
}
}
diff --git a/src/locales/fr.json b/src/locales/fr.json
index bc9c9d66..5fcb95ab 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -6,8 +6,8 @@
"about": {
"header": "À Propos",
"p1": "<strong>coinos</strong> est un portefeuille, point de vente, marché d'e-commerce et plateforme d'échange de bitcoin <strong>open source</strong>. Development a commencé en Vancouver le septembre de 2012 pour donner aux marchands du coin un façon pratique d'accepter des paiements en bitcoin.",
- "p2": "Il a eu plus de fonctions au fil des ans pour satisfaire une grande variété de cas d'utilisation avec le but de rendre commencer utiliser bitcoin rapidement, à bas prix, et en sécurité aussi façile que possible pour n'importe qui.",
- "p3": "Tout le monde peux télécharger le code source et héberger leur propre instance de la plateforme soutenu par leur propres nœuds de bitcoin, liquid ou lightning, ou contactez-nous et nous l'installalons pour vous avec plaisir.",
+ "p2": "Il a eu plus de fonctions au fil des ans pour satisfaire une grande variété de cas d'utilisation dans le but de pouvoir utiliser bitcoin rapidement, à bas prix, et en sécurité aussi façile que possible pour n'importe qui.",
+ "p3": "Tout le monde peux télécharger le code source et héberger leur propre instance de la plateforme soutenue par leur propres nœuds de bitcoin, liquid ou lightning, ou contactez-nous et nous l'installerons pour vous avec plaisir.",
"locations": {
"header": "Établissements",
"description": "Il y a des entreprises qui acceptent bitcoin utilisant le point de vente coinos. Si vous utilisez notre appli, s'il vous plait <a href=\"mailto:[email protected]\" class=\"underline underline-offset-4\">fais-le-nous savoir!</a>"
@@ -17,12 +17,12 @@
"adam": {
"name": "Adam Soltys",
"position": "Officier Technique Principal",
- "bio": "Adam est programmeur de web et passionné de bitcoin. Il croit que l'argent sûr est clé à augmenter la prospérité planétaire. Il habite en Vancouver de Nord avec sa femme Chelsea et deux enfants."
+ "bio": "Adam est développeur web et passionné de bitcoin. Il croit que l'argent sûr est la clé pour augmenter la prospérité planétaire. Il habite en Vancouver de Nord avec sa femme Chelsea et deux enfants."
},
"secondl1ght": {
"name": "secondl1ght",
- "position": "Programmeur de Web",
- "bio": "Secondl1ght est un programmeur de web autodidacte. Il a découvert bitcoin et quitté son job de monnaie fiduciaire pour développer bitcoin à temps plein. Il contribue aux projets open source, est défenseur d'intimité, et il aime la liberté."
+ "position": "Développeur Web",
+ "bio": "Secondl1ght est un développeur web autodidacte. Il a découvert bitcoin et quitté son job de monnaie fiduciaire pour développer bitcoin à temps plein. Il contribue aux projets open source, est défenseur de la vie privée, et il aime la liberté."
},
"karnage": {
"name": "Karnage",
@@ -32,7 +32,7 @@
"shauna": {
"name": "Shauna",
"position": "Gestion & Marketing",
- "bio": "Gestion de projets et strategie. Touche-à-tout. Peux toujours trouver le temps pour racontrer pour café."
+ "bio": "Gestion de projets et strategie. Touche-à-tout. Peut toujours trouver le temps pour racontrer pour café."
},
"cole": {
"name": "Cole Goodwin",
@@ -41,8 +41,8 @@
},
"john": {
"name": "John Barratt",
- "position": "Programmeur de Web Full-Stack",
- "bio": "Quand il ne programme pas, vous pouvez trouver John au forêt randonnant au bonnes sentiers de Colombie-Britannique."
+ "position": "Développeur Web Full-Stack",
+ "bio": "Quand il ne programme pas, vous pouvez trouver John en forêt randonnant sur les sentiers de Colombie-Britannique."
}
}
},
@@ -54,16 +54,16 @@
"faq": {
"header": "FAQ",
"cost": {
- "question": "Qu'est-ce que c'est le coût?",
- "answer": "coinos est gratuit à utiliser. Vous seulment payez un prix 1% quand vous convertez vos fonds entre bitcoin, Liquid et Lightning. Comparez-ça à les frais de dossir typiques 3% des cartes credit."
+ "question": "Quel est le coût?",
+ "answer": "coinos est gratuit à utiliser. Vous payez seulment 1% quand vous convertissez vos fonds entre bitcoin, Liquid et Lightning. Comparez-ça aux frais typiques 3% des cartes de crédit."
},
"compatibility": {
- "question": "Est-ce que j'ai besoin du logiciel ou appareil special?",
- "answer": "Non. coinos est un site Web, alors vous pouvez l'utiliser sur n'importe quelle appareil tant que vous aves un connexion d'Internet. La plupart des marchands l'utilise sur un téléphone ou une tablette."
+ "question": "Est-ce que j'ai besoin de logiciel ou d'appareil special ?",
+ "answer": "Non. coinos est un site Web, vous pouvez donc l'utiliser sur n'importe quel appareil tant que vous aves un connexion d'Internet. La plupart des marchands l'utilise sur un téléphone ou une tablette."
},
"safety": {
- "question": "Sont mes fonds saufs?",
- "answer": "Vos fonds sont entreposés en notre serveur sécurisé, mais nous recommendons que vous les retirez à votre propre entreposage pour les sauvegarder à long terme."
+ "question": "Sont mes fonds saufs ?",
+ "answer": "Vos fonds sont entreposés sur notre serveur sécurisé, mais nous recommandons que vous les retiriez vers votre propre entreposage pour les sauvegarder à long terme."
}
},
"footer": {
@@ -77,10 +77,10 @@
"news": "Nouvelles"
},
"howItWorks": {
- "header": "Comment Elle Fonctionne",
+ "header": "Comment cela Fonctionne",
"step1": {
- "header": "1. Crée votre compte",
- "description": "Un nom d'utilateur et mot de passe est tout qui est besoin pour commencer."
+ "header": "1. Créez votre compte",
+ "description": "Un nom d'utilateur et mot de passe est tout ce dont vous avez besoin pour commencer."
},
"step2": {
"header": "2. Demande pour des paiements",
@@ -88,46 +88,46 @@
},
"step3": {
"header": "3. C'est tout!",
- "description": "Vous pouvez convertir au monnaie local n'importe quand."
+ "description": "Vous pouvez convertir en monnaie local n'importe quand."
}
},
"landing": {
"header_before_icon": "Le",
"header_with_icon": "façon",
- "header_after_icon": "le plus façile d'accepter bitcoin en votre magasin.",
- "subheader": "Aggrandir votre clientèle et chiffre d'affaires par accepter bitcoin avec un point de vente façile à utiliser et dirigé. Logiciel et matériel informatique non obligatoire.",
+ "header_after_icon": "le plus façile d'accepter bitcoin dans votre magasin.",
+ "subheader": "Agrandir votre clientèle et chiffre d'affaires en acceptant bitcoin avec un point de vente façile à utiliser et géré. Logiciel et matériel informatique non obligatoire.",
"info1": {
- "title": "Paiements instants avec Lightning",
- "description": "Le moment que votre client envoie un paiment, c'est la. Vous ne devez pas attendre pour les décisions de banques ou vous inquieter pour des rejets de débit. Décisions instant, en secondes. Dis au revoir aux frais de dossir 3% des cartes de crédit."
+ "title": "Paiements instantanés avec Lightning",
+ "description": "Le moment où votre client envoie un paiment, c'est là. Vous ne devez pas attendre les décisions des banques ou vous inquiéter pour des rejets de débit. Décisions instantanées, en secondes. Dites au revoir aux frais de transactions de 3% des cartes de crédit."
},
"info2": {
"title": "Simple à utiliser, en garde par défaut",
"description1": "Juste entrer une quantité, partager le code QR et c'est tout!",
- "description2": "Tes contes sont en garde par défaut, qui élimine le nécessité de comprendre comment bitcoin/lightning fonctionnent. Nous réalisons le plus gros du travail pour que vous pouvez vous concentrer sur votre entreprise.",
- "description3": "Vous toujours avez l'option de <span class=\"underline undeline-offset-4\">prendre garde</span> de vos clés."
+ "description2": "Tes comptes sont en garde par défaut, qui élimine le nécessité de comprendre comment bitcoin/lightning fonctionnent. Nous réalisons le plus gros du travail pour que vous puissiez vous concentrer sur votre entreprise.",
+ "description3": "Vous avez toujours l'option de <span class=\"underline undeline-offset-4\">gérer</span> vos clés."
},
"info3": {
- "title": "Personalise-le comment vous voulez",
- "description": "Personalisez votre point de vente, ou rettenez-le au défaut - c'est votre chois."
+ "title": "Personalisez-le comme vous voulez",
+ "description": "Personalisez votre point de vente, ou utilisez les réglages par défaut - c'est votre chois."
}
},
"login": {
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"email": "Courriel",
- "passwordRecommendation": "Nous recommendons un mot de passe long et au hazard, généré par un gestionnaire de mots de passe.",
- "forgotUserOrPassword": "Oublié ton nom d'utilisateur ou mot de passe?",
- "haveAccount": "Avez-vous déjà une conte?",
- "noAccount": "N'avez-vous pas un conte?",
+ "passwordRecommendation": "Nous recommendons un mot de passe long et aléatoire, généré par un gestionnaire de mots de passe.",
+ "forgotUserOrPassword": "Oublié votre nom d'utilisateur ou mot de passe ?",
+ "haveAccount": "Avez-vous déjà un compte ?",
+ "noAccount": "Pas encore de compte ?",
"register": "Enregistrer",
"signIn": "Connexion",
- "failed": "Connexion failli",
+ "failed": "Echec de Connexion",
"optional": "Optionnel",
- "token": "Jeton A2F"
+ "token": "Jeton 2FA"
},
"nav": {
"startInSeconds": "Commence en Secondes",
- "account": "Conte",
+ "account": "Compte",
"signIn": "Connexion",
"signOut": "Déconnexion",
"dashboard": "Tableau de bord",
@@ -135,7 +135,7 @@
"support": "Assistance",
"profile": "Profil"
},
- "newSiteHeader": "Bienvenue à la <a href=\"/news\" class=\"underline underline-offset-4 text-black hover:opacity-80\">coinos nouvelle et amélioré</a>, concentré au marchants! Cherchez pour le dernier coinos? Visitez <a href=\"https://classic.coinos.io\" class=\"underline underline-offset-4 text-black hover:opacity-80\">classic.coinos.io</a>.",
+ "newSiteHeader": "Bienvenue sur <a href=\"/news\" class=\"underline underline-offset-4 text-black hover:opacity-80\">coinos nouveau et amélioré</a>, focalisé sur les marchants! Vous cherchez l'ancien coinos? Visitez <a href=\"https://classic.coinos.io\" class=\"underline underline-offset-4 text-black hover:opacity-80\">classic.coinos.io</a>.",
"invoice": {
"addTip": "Ajoutez un pourboire",
"addTipq": "Ajouter un pourboire?",
@@ -161,7 +161,7 @@
"dashboard": "Tableau de bord",
"accountBalance": "Solde",
"bitcoinPrice": "Prix de bitcoin:",
- "conversionFeeWarning": "Il y a un prix de 1% pour convertir entre bitcoin, Liquid et Lightning.",
+ "conversionFeeWarning": "Il y a des frais de 1% pour convertir entre bitcoin, Liquid et Lightning.",
"dataFromBinance": "Données de <span class=\"text-black\">Binance</span>.",
"fetchingRate": "cherchant taut de change...",
"optionalNote": "Note (optionel)",
@@ -174,21 +174,21 @@
},
"receive": {
"lessThan1BTCWarning": "S'il vous plaît utilisez moins d'un bitcoin.",
- "showQR": "Voire QR"
+ "showQR": "Voire le QR code"
},
"settings": {
"header": "Préférences",
- "ACCOUNT": "CONTE",
+ "ACCOUNT": "COMPTE",
"POINT_OF_SALE": "POINT DE VENTE",
"SECURITY": "SÉCURITÉ",
"locale": "Langue",
"username": "Nom d'Utilisateur",
"email": "Courriel",
- "newPassword": "Mot de Passe Nouveau",
+ "newPassword": "Nouveau Mot de Passe",
"emailNotifications": "Notifications par Courriel",
- "emailNotificationsDescription": "Nous pouvons vous notifier par courriel quand votre conte change ou quand vous recevez des paiements",
+ "emailNotificationsDescription": "Nous pouvons vous notifier par courriel quand votre compte change ou quand vous recevez des paiements",
"smsNotifications": "Notifications par SMS",
- "smsNotificationsDescription": "Nous vous envoyons des notifications par texte quand ceci est activé",
+ "smsNotificationsDescription": "Nous vous envoyons des notifications par texte quand c'est activé",
"theme": "Thème",
"themeDescription": "Sélectionnez des thèmes dèfauts.",
"localCurrency": "Monnaie Local",
@@ -263,13 +263,13 @@
"export": "Exportez",
"invoice": "Facture",
"readyToPay": "est prêt à payer",
- "describeOrder": "Utilizer ce champ pour décrire votre commande",
+ "describeOrder": "Utiliser ce champ pour décrire votre commande",
"requestAnInvoice": "Demandez une facture",
"submit": "Soumettre",
"isRequesting": "demande",
"invoiceFrom": "Facture de",
"awaiting": "Attendez pour",
- "tapAnywhere": "Tapez partout",
+ "tapAnywhere": "Tapez n'importe où",
"from": "de",
"redeem": "Échangez",
"redeemed": "Déjà échangé",
diff --git a/src/locales/zh.json b/src/locales/zh.json
new file mode 100644
index 00000000..402f0214
--- /dev/null
+++ b/src/locales/zh.json
@@ -0,0 +1,291 @@
+{
+ "404": {
+ "404": "404",
+ "pageNotFound": "Page not found."
+ },
+ "about": {
+ "header": "About",
+ "p1": "Coinos is an open source bitcoin payment app. Development started in Vancouver in September 2012 as a way to provide local merchants with a convenient way to accept bitcoin payments.",
+ "p2": "You can download the source code to run an instance and connect it to your own bitcoin and lightning nodes. We'll help you get started being a benevolent custodian and payments provider for your community.",
+ "p3": "Account transfers amongst users on our server are done off-chain so they're completely free, instant, and unlimited.",
+ "team": {
+ "header": "The Team",
+ "adam": {
+ "name": "Adam Soltys",
+ "position": "Chief Technical Officer",
+ "bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children."
+ },
+ "shauna": {
+ "name": "Shauna",
+ "position": "Operations & Marketing",
+ "bio": "Strategic & Project Management. Jack of all Trades. Can always find time to meet for coffee."
+ },
+ "cole": {
+ "name": "Cole",
+ "position": "Certified Bitcoin Professional",
+ "bio": "Cole is a driven individual with a passion for freedom. He came across bitcoin in 2016 and studied it in depth. Now he wants to introduce the lightning network to as many users as possible."
+ },
+ "john": {
+ "name": "John Barratt",
+ "position": "Full Stack Web Developer",
+ "bio": "When not coding you can find John out in the woods hiking all the great trails that BC has to offer."
+ }
+ },
+ "locations": {
+ "header": "Locations",
+ "description": "Here are some businesses that are accepting bitcoin with Coinos. We hope that yours will be next!"
+ }
+ },
+ "error": {
+ "header": "Error",
+ "message": "Please try again or contact support.",
+ "signIn": "Please sign in to continue."
+ },
+ "faq": {
+ "header": "FAQ",
+ "cost": {
+ "question": "What does it cost?",
+ "answer": "It's free to register an account and receive payments over the lightning network. We charge a 0.1% fee to convert from lightning to bitcoin."
+ },
+ "compatibility": {
+ "question": "Do I need any special device or software?",
+ "answer": "No. Coinos is web-based, so as long as you have an internet connection, you can use it on any device. Retail merchants might use it on a phone or a tablet."
+ },
+ "safety": {
+ "question": "When do my funds settle?",
+ "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet or exchange right after payments are received."
+ }
+ },
+ "footer": {
+ "company": "Company",
+ "followUs": "Follow Us",
+ "github": "GitHub",
+ "jobs": "Jobs",
+ "support": "Support",
+ "telegram": "Telegram",
+ "twitter": "Twitter",
+ "documentation": "Documentation"
+ },
+ "howItWorks": {
+ "header": "How it works",
+ "step1": {
+ "header": "1. Create your account",
+ "description": "A username and password is all you need to get started."
+ },
+ "step2": {
+ "header": "2. Ask for payments",
+ "description": "The same as you normally would, but with bitcoin."
+ },
+ "step3": {
+ "header": "3. That's all!",
+ "description": "Spend your coin at other merchants or save it for a rainy day, the choice is yours."
+ }
+ },
+ "invoice": {
+ "addTip": "Add a tip",
+ "addTipq": "Add a tip?",
+ "done": "Done",
+ "paymentSuccessful": "Payment received!",
+ "invoice": "Invoice",
+ "total": "Total",
+ "tip": "Tip"
+ },
+ "jobs": {
+ "header": "Jobs",
+ "subheader": "Current Openings",
+ "pitch1": "Coinos is actively looking for developers to join our growing team (full-time or part-time, remote working OK).",
+ "pitch2": "Excited about working on open source bitcoin? Then take a look at our repositories on",
+ "github": "GitHub",
+ "pitch3": "If you think you can make an impact, we'd love to hear from you. Send us your resume to"
+ },
+ "landing": {
+ "header_before_icon": "The",
+ "header_with_icon": "easiest",
+ "header_after_icon": "way to accept bitcoin in your store.",
+ "subheader": "Grow your customer base and revenue by accepting bitcoin with an easy-to-use web app. No software or hardware required.",
+ "info1": {
+ "title": "Instant payments via Lightning",
+ "description": "The moment your customer sends a payment, it’s there. No waiting for bank settlements or worrying about chargebacks. Instant settlement, in seconds. Say goodbye to 3% credit card processing fees."
+ },
+ "info2": {
+ "title": "Simple to use",
+ "description1": "Just enter an amount in your local currency, share the QR code and you’re done.",
+ "description2": "We do all the heavy lifting so you can focus on your business.",
+ "description3": "You always have the option of taking custody of your funds by withdrawing instantly at any time."
+ },
+ "info3": {
+ "title": "Brand it your way!",
+ "description": "Customize your point of sale look and feel, or keep it on default - it's totally up to you."
+ }
+ },
+ "login": {
+ "optional": "Optional",
+ "token": "2FA Token",
+ "username": "Username",
+ "password": "Password",
+ "email": "Email",
+ "passwordRecommendation": "We recommend a long, random password generated with a password manager.",
+ "forgotUserOrPassword": "Forgot Username or Password?",
+ "haveAccount": "Already have an account?",
+ "noAccount": "Don't have an account?",
+ "register": "Register",
+ "signIn": "Sign In",
+ "failed": "Login failed"
+ },
+ "nav": {
+ "profile": "Profile",
+ "startInSeconds": "Start in Seconds",
+ "account": "Account",
+ "signIn": "Sign In",
+ "signOut": "Sign Out",
+ "dashboard": "Dashboard",
+ "settings": "Settings",
+ "support": "Support"
+ },
+ "newSiteHeader": "Join us at our <a href=\"/launch\" class=\"underline underline-offset-4 text-black hover:opacity-80\">launch party</a> in Vancouver on October 26! Looking for the old coinos? Visit <a href=\"https://classic.coinos.io\" class=\"underline underline-offset-4 text-black hover:opacity-80\">classic.coinos.io</a>",
+ "news": {
+ "header": "News",
+ "subheader": "Latest Updates",
+ "update1": ""
+ },
+ "user": {
+ "bitcoin": "Bitcoin",
+ "lightning": "Lightning",
+ "invoice": "Invoice",
+ "unfollow": "Unfollow",
+ "following": "Following",
+ "follow": "Follow",
+ "followers": "Followers",
+ "remove": "Remove",
+ "noNotes": "No notes yet",
+ "message": "Message",
+ "messages": "Messages",
+ "feed": "Feed",
+ "notes": "Notes",
+ "pay": "Pay",
+ "scanToPayBTC": "Scan to pay with bitcoin",
+ "this": "This",
+ "lightningAddress": "lightning address",
+ "likeAnEmail": "is like an email for bitcoin. You can publish it on your website or business cards and use it multiple times.",
+ "dashboard": {
+ "go": "Go",
+ "dashboard": "Dashboard",
+ "accountBalance": "Account Balance",
+ "bitcoinPrice": "bitcoin Price:",
+ "conversionFeeWarning": "You will be charged a 1% withdrawal fee on the amount.",
+ "dataFromBinance": "Data from <span class=\"text-black\">Binance</span>.",
+ "fetchingRate": "fetching rate...",
+ "optionalNote": "Note (Optional)",
+ "send": "Send",
+ "receive": "Receive",
+ "withdraw": "Send",
+ "deposit": "Deposit",
+ "voucher": "Voucher"
+ },
+ "receive": {
+ "lessThan1BTCWarning": "Please enter an amount less than 1 bitcoin.",
+ "showQR": "Show QR"
+ },
+ "send": {
+ "contacts": "Contacts",
+ "scan": "Scan",
+ "paste": "Paste",
+ "next": "Next",
+ "placeholder": "Paste a bitcoin address, lightning invoice or coinos username"
+ },
+ "settings": {
+ "bannerImage": "Banner Image",
+ "profileImage": "Profile Image",
+ "oneTimeCode": "Enter one-time code",
+ "accountId": "Account Secret",
+ "header": "Settings",
+ "ACCOUNT": "ACCOUNT",
+ "POINT_OF_SALE": "PREFERENCES",
+ "SECURITY": "SECURITY",
+ "locale": "Language",
+ "username": "Username",
+ "email": "Email",
+ "newPassword": "Change Password",
+ "about": "About",
+ "aboutPlaceholder": "A little something about you or your business address and hours",
+ "emailNotifications": "Email Notifications",
+ "emailNotificationsDescription": "Get notified via email when a change occurs on your account or when people send you payments.",
+ "smsNotifications": "SMS Notifications",
+ "smsNotificationsDescription": "Coinos will send you text notifications when this is enabled.",
+ "theme": "Color Theme",
+ "tipPrompt": "Prompt for tips",
+ "tipPromptDescription": "Ask people if they want to add a tip before paying their invoices",
+ "displayName": "Display name",
+ "themeDescription": "Select from the default color themes.",
+ "localCurrency": "Currency",
+ "btcUnit": "Display bitcoin in:",
+ "bitcoin": "bitcoin",
+ "minutes": "minutes",
+ "hour": "hour",
+ "hours": "hours",
+ "satoshis": "Satoshis",
+ "securityPIN": "Security PIN",
+ "securityPINDescription": "Set a security pin that will be used to unlock your settings and to send outgoing payments.",
+ "setPIN": "Set PIN",
+ "autoLock": "Auto-Lock Timer",
+ "autoLockDescription": "Your account will go into a secure mode, requiring you to re-enter your credentials after a specified time:",
+ "twofa": "2-Factor Authentication",
+ "twofaDescription": "Add an extra level of security to your device by enabling 2-factor authentication.",
+ "twofaSetup": "Set up 2FA",
+ "twofaDisable": "Disable 2FA",
+ "select": "Select",
+ "verifyPIN": "Verify PIN",
+ "enablePIN": "Enable PIN",
+ "disablePIN": "Disable PIN"
+ },
+ "support": {
+ "header": "Support",
+ "email": "Email",
+ "accountName": "Account Name",
+ "message": "Message",
+ "send": "Send"
+ }
+ },
+ "payments": {
+ "amount": "Amount",
+ "date": "Date",
+ "id": "Transaction ID",
+ "details": "Transaction Details",
+ "header": "Payments",
+ "preimage": "Proof of payment",
+ "type": "Type",
+ "time": "Time",
+ "sent": "Sent",
+ "received": "Received",
+ "empty": "No transactions yet.",
+ "send": "Send",
+ "next": "Next",
+ "sendersChoice": "Sender's choice",
+ "sendTo": "Send to",
+ "to": "to",
+ "pay": "Pay",
+ "requesting": "is requesting",
+ "sentInvoiceTo": "Sent invoice to",
+ "sentRequestTo": "Request sent!",
+ "pleasePay": "Please pay",
+ "day": "Day",
+ "week": "Week",
+ "month": "Month",
+ "all": "All",
+ "export": "Export",
+ "invoice": "Invoice",
+ "readyToPay": "is ready to pay",
+ "describeOrder": "Use this field to describe your order",
+ "requestAnInvoice": "Request Invoice",
+ "submit": "Submit",
+ "isRequesting": "is requesting",
+ "invoiceFrom": "Invoice from",
+ "awaiting": "Awaiting",
+ "tapAnywhere": "Tap anywhere to continue",
+ "from": "from",
+ "redeem": "Redeem",
+ "redeemed": "Redeemed",
+ "redeemedBy": "Redeemed by",
+ "sendMessage": "Write a message..."
+ }
+}
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="&bull;&bull;&bull;"
+ 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="&bull;&bull;&bull;"
+ 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"
> 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 &mdash; 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: [] };
diff --git a/static/manifest.webmanifest b/static/manifest.webmanifest
index f77d93c7..c9243a94 100644
--- a/static/manifest.webmanifest
+++ b/static/manifest.webmanifest
@@ -28,5 +28,11 @@
"sizes": "512x512",
"type": "image/png"
}
+ ],
+ "protocol_handlers": [
+ {
+ "protocol": "bitcoin",
+ "url": "/send/%s"
+ }
]
}
diff --git a/static/tickets/01.png b/static/tickets/01.png
deleted file mode 100644
index 555904c0..00000000
--- a/static/tickets/01.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/02.png b/static/tickets/02.png
deleted file mode 100644
index 65d35ba2..00000000
--- a/static/tickets/02.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/03.png b/static/tickets/03.png
deleted file mode 100644
index b073cef5..00000000
--- a/static/tickets/03.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/04.png b/static/tickets/04.png
deleted file mode 100644
index 14d6b567..00000000
--- a/static/tickets/04.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/05.png b/static/tickets/05.png
deleted file mode 100644
index 99351b3d..00000000
--- a/static/tickets/05.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/06.png b/static/tickets/06.png
deleted file mode 100644
index 366747ac..00000000
--- a/static/tickets/06.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/07.png b/static/tickets/07.png
deleted file mode 100644
index f774c683..00000000
--- a/static/tickets/07.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/08.png b/static/tickets/08.png
deleted file mode 100644
index 4f8ad5c3..00000000
--- a/static/tickets/08.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/09.png b/static/tickets/09.png
deleted file mode 100644
index f8fd9cf6..00000000
--- a/static/tickets/09.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/10.png b/static/tickets/10.png
deleted file mode 100644
index d557099c..00000000
--- a/static/tickets/10.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/11.png b/static/tickets/11.png
deleted file mode 100644
index 484150a6..00000000
--- a/static/tickets/11.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/12.png b/static/tickets/12.png
deleted file mode 100644
index 05b4a337..00000000
--- a/static/tickets/12.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/13.png b/static/tickets/13.png
deleted file mode 100644
index a853d777..00000000
--- a/static/tickets/13.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/14.png b/static/tickets/14.png
deleted file mode 100644
index 3ad8105a..00000000
--- a/static/tickets/14.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/15.png b/static/tickets/15.png
deleted file mode 100644
index d34eed52..00000000
--- a/static/tickets/15.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/16.png b/static/tickets/16.png
deleted file mode 100644
index 8a1b15b6..00000000
--- a/static/tickets/16.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/17.png b/static/tickets/17.png
deleted file mode 100644
index a763b6c9..00000000
--- a/static/tickets/17.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/18.png b/static/tickets/18.png
deleted file mode 100644
index 14811954..00000000
--- a/static/tickets/18.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/19.png b/static/tickets/19.png
deleted file mode 100644
index 04c3915b..00000000
--- a/static/tickets/19.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/20.png b/static/tickets/20.png
deleted file mode 100644
index c42c311d..00000000
--- a/static/tickets/20.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/21.png b/static/tickets/21.png
deleted file mode 100644
index 3863d45e..00000000
--- a/static/tickets/21.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/22.png b/static/tickets/22.png
deleted file mode 100644
index 0e8cb413..00000000
--- a/static/tickets/22.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/23.png b/static/tickets/23.png
deleted file mode 100644
index bac50642..00000000
--- a/static/tickets/23.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/24.png b/static/tickets/24.png
deleted file mode 100644
index 140abd01..00000000
--- a/static/tickets/24.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/25.png b/static/tickets/25.png
deleted file mode 100644
index fb5d5b32..00000000
--- a/static/tickets/25.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/26.png b/static/tickets/26.png
deleted file mode 100644
index 980f824f..00000000
--- a/static/tickets/26.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/27.png b/static/tickets/27.png
deleted file mode 100644
index 6892cd7d..00000000
--- a/static/tickets/27.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/28.png b/static/tickets/28.png
deleted file mode 100644
index 03f1b0fa..00000000
--- a/static/tickets/28.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/29.png b/static/tickets/29.png
deleted file mode 100644
index 39a1eaff..00000000
--- a/static/tickets/29.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/30.png b/static/tickets/30.png
deleted file mode 100644
index 35f7463a..00000000
--- a/static/tickets/30.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/31.png b/static/tickets/31.png
deleted file mode 100644
index 2f7a6b2c..00000000
--- a/static/tickets/31.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/32.png b/static/tickets/32.png
deleted file mode 100644
index cae96a35..00000000
--- a/static/tickets/32.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/33.png b/static/tickets/33.png
deleted file mode 100644
index 1754a731..00000000
--- a/static/tickets/33.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/34.png b/static/tickets/34.png
deleted file mode 100644
index 4d3eb544..00000000
--- a/static/tickets/34.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/35.png b/static/tickets/35.png
deleted file mode 100644
index be8775dc..00000000
--- a/static/tickets/35.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/36.png b/static/tickets/36.png
deleted file mode 100644
index a80c1b39..00000000
--- a/static/tickets/36.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/37.png b/static/tickets/37.png
deleted file mode 100644
index a6536293..00000000
--- a/static/tickets/37.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/38.png b/static/tickets/38.png
deleted file mode 100644
index 06a22e8f..00000000
--- a/static/tickets/38.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/39.png b/static/tickets/39.png
deleted file mode 100644
index a45ce455..00000000
--- a/static/tickets/39.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/40.png b/static/tickets/40.png
deleted file mode 100644
index acfd2a9a..00000000
--- a/static/tickets/40.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/41.png b/static/tickets/41.png
deleted file mode 100644
index 5e98fa33..00000000
--- a/static/tickets/41.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/42.png b/static/tickets/42.png
deleted file mode 100644
index eeefd71b..00000000
--- a/static/tickets/42.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/43.png b/static/tickets/43.png
deleted file mode 100644
index c1ce6eac..00000000
--- a/static/tickets/43.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/44.png b/static/tickets/44.png
deleted file mode 100644
index 36b79835..00000000
--- a/static/tickets/44.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/45.png b/static/tickets/45.png
deleted file mode 100644
index f1889622..00000000
--- a/static/tickets/45.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/46.png b/static/tickets/46.png
deleted file mode 100644
index 1a74008f..00000000
--- a/static/tickets/46.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/47.png b/static/tickets/47.png
deleted file mode 100644
index 0d9bb11c..00000000
--- a/static/tickets/47.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/48.png b/static/tickets/48.png
deleted file mode 100644
index 3f4c9b1f..00000000
--- a/static/tickets/48.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/49.png b/static/tickets/49.png
deleted file mode 100644
index 0b88dc78..00000000
--- a/static/tickets/49.png
+++ /dev/null
Binary files differ
diff --git a/static/tickets/50.png b/static/tickets/50.png
deleted file mode 100644
index bd2aa215..00000000
--- a/static/tickets/50.png
+++ /dev/null
Binary files differ
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 2941c890..c57db020 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -12,5 +12,6 @@ module.exports = {
}
}
},
- plugins: []
+ plugins: [require('tailwind-scrollbar')],
+ safelist: ['w-16', 'h-16', 'w-20', 'h-20', 'w-24', 'h-24']
};