diff options
| author | Adrien Hopkins <[email protected]> | 2022-08-02 13:13:27 -0500 |
|---|---|---|
| committer | Adrien Hopkins <[email protected]> | 2022-08-02 13:13:27 -0500 |
| commit | c21ee41b206db79b2157d8743b0473a094448dfb (patch) | |
| tree | fac1a595e451c0b32ceedfe4fb70855270456377 /src | |
| parent | 3b216dd6c170a7d71d21a391098c554008c8bdb4 (diff) | |
| parent | 05f5001fc7d325e31d9c777be11343604c72fbfd (diff) | |
| download | coinos-ui-c21ee41b206db79b2157d8743b0473a094448dfb.tar.gz coinos-ui-c21ee41b206db79b2157d8743b0473a094448dfb.zip | |
Merge branch 'master' into i18n
Diffstat (limited to 'src')
38 files changed, 1357 insertions, 213 deletions
diff --git a/src/components/404.svelte b/src/components/404.svelte new file mode 100644 index 00000000..503316bb --- /dev/null +++ b/src/components/404.svelte @@ -0,0 +1,16 @@ +<script> + import { Icon } from '$comp'; +</script> + +<div class="flex justify-center items-center mt-20"> + <div class="space-y-5 text-center"> + <a href="/"> + <Icon icon="logo" /> + </a> + + <Icon icon="question" style="mx-auto w-full" /> + + <h1 class="font-bold text-5xl">404</h1> + <p>Page not found.</p> + </div> +</div> diff --git a/src/components/About.svelte b/src/components/About.svelte new file mode 100644 index 00000000..3731bf9b --- /dev/null +++ b/src/components/About.svelte @@ -0,0 +1,73 @@ +<script> + import { Image, Icon } from '$comp'; + + const aboutAdam = [ + 'Running Bitcoin since 2011', + '20+ years web development experience in government and industry', + 'Early Bitcoin Vancouver meetup organizer', + 'Founder of The Bitcoin Co-op and early supporter of Decentral Vancouver', + 'Yogi, permaculturist, anarchist, decentralist', + 'Husband, father, brother, son' + ]; +</script> + +<h3 class="text-5xl font-medium mb-10 text-center">About</h3> +<div + class="md:px-10 lg:px-0 block lg:flex justify-center text-secondary text-xl lg:space-x-20 mx-auto max-w-7xl relative" +> + <p class="w-full lg:w-5/12"> + <strong>CoinOS is</strong> an <strong>open source</strong> bitcoin web wallet, point of sale, + ecommerce marketplace and exchange platform. Development started in Vancouver in September 2012 + as a way to provide local merchants with a convenient way to accept bitcoin payments. + <br /><br /> + The feature set has grown over the years to accomodate a wide variety of use cases with the goal + of making it as easy as possible for anyone to get started using Bitcoin quickly, cheaply, and securely. + <br /><br /> + </p> + <p class="w-full lg:w-5/12"> + Anyone can download the source code and host their own instance of the platform backed by their + own bitcoin, liquid or lightning nodes, or reach out and we'll gladly set this up for you. + <br /><br /> + Intra-account transfers between custodial wallets of different users on our server are completely + free, instant, and unlimited. + </p> + <Image image="swoosh" style="w-32 absolute -bottom-20 lg:bottom-0 right-0 md:right-10" /> +</div> + +<div class="px-5 block lg:flex justify-center lg:space-x-32 items-center mt-40 lg:mt-20"> + <div class="space-y-5 mb-5 lg:mb-0"> + <h3 class="text-5xl font-medium mb-10 text-center lg:text-right">The Founder</h3> + + <Image image="adam" style="rounded-full mx-auto lg:mx-0 lg:ml-auto w-40" /> + + <div class="space-y-2"> + <span class="text-2xl text-center font-semibold block lg:text-right">Adam Soltys</span> + <span class="text-xl text-center text-secondary block lg:text-right" + >Chief Technical Officer</span + > + </div> + + <div class="flex justify-center lg:justify-end items-center space-x-5"> + <a + href="https://twitter.com/adamsoltys" + target="_blank" + rel="noreferrer" + class="rounded-full p-2 bg-[#1D9BF0]"><Icon icon="twitter" style="w-6" /></a + > + <a + href="https://github.com/asoltys" + target="_blank" + rel="noreferrer" + class="rounded-full p-2 bg-gray-800"><Icon icon="github" style="w-6" /></a + > + </div> + </div> + + <ul + class="w-full mx-auto md:w-[700px] lg:w-[500px] xl:w-auto text-secondary px-1 md:px-10 text-xl list-disc space-y-5" + > + {#each aboutAdam as about} + <li>{about}</li> + {/each} + </ul> +</div> diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte index 249e2826..242e6924 100644 --- a/src/components/AppHeader.svelte +++ b/src/components/AppHeader.svelte @@ -1,32 +1,88 @@ <script> // we will want to dynamically use the users avatar on this screen if they have uploaded one + import { user } from '$lib/store'; import { goto } from '$app/navigation'; import { Icon, LocaleSelector } from '$comp'; - import { locale, locales } from 'svelte-i18n'; + export let avatarPosition = 'left-[calc(50vw-48px)]'; + + const menuButtons = [ + { title: 'Customers', icon: 'customers', goto: 'customers' }, + { title: 'Dashboard', icon: 'dash', goto: 'dashboard' }, + { title: 'Settings', icon: 'settings', goto: 'settings' }, + { title: 'Support', icon: 'support', goto: 'support' }, + { title: 'Sign out', icon: 'logout', goto: 'logout' } + ]; + + let showMenu = false; + + const handleMenuItemClick = (item) => { + if (item === 'logout') { + goto('/logout'); + } else { + goto(`/${$user.username + '/' + item}`); + } + }; </script> -<header class="bg-gradient-to-r from-primary to-gradient h-[20vh] w-full relative"> +<header class="bg-gradient-to-r from-primary to-gradient h-[175px] w-full relative"> <nav class="flex justify-end items-center space-x-4 p-5"> <LocaleSelector /> - - <button class="bg-white p-2 rounded-full w-12 h-12" on:click={() => goto('/receive')} - ><Icon icon="numpad" style="mx-auto" /></button - > - <button class="bg-white p-2 rounded-full w-12 h-12" - ><Icon icon="clock" style="mx-auto" /></button - > - <button class="bg-white p-2 rounded-full w-12 h-12" - ><Icon icon="menu" style="mx-auto" /></button - > - <button class="bg-white p-2 rounded-full w-12 h-12" - ><Icon icon="profile" style="mx-auto" /></button - > + {#if $user} + <a href={`/${$user.username}/receive`}> + <button class="bg-white p-2 rounded-full w-12 h-12" + ><Icon icon="numpad" style="mx-auto" /> + </button> + </a> + <a href={`/${$user.username}/transactions`}> + <button class="bg-white p-2 rounded-full w-12 h-12" + ><Icon icon="clock" style="mx-auto" /> + </button> + </a> + <a href={`/${$user.username}`}> + <button class="bg-white p-2 rounded-full w-12 h-12" + ><Icon icon="profile" style="mx-auto" /> + </button> + </a> + <div class="relative"> + <button class="bg-white p-2 rounded-full w-12 h-12" on:click={() => (showMenu = !showMenu)} + ><Icon icon="menu" style="mx-auto" /> + </button> + <div + class="{showMenu + ? 'block' + : 'hidden'} absolute top-14 right-0 bg-white rounded-3xl p-8 shadow-xl z" + > + <ul class="space-y-5 w-32"> + {#each menuButtons as button} + <li> + <button + class="flex justify-center items-center font-semibold text-sm" + on:click={() => handleMenuItemClick(button.goto)} + ><Icon icon={button.icon} style="mr-1" /> {button.title} + </button> + </li> + {/each} + </ul> + </div> + </div> + {:else} + <a href="/login"> + <button class="bg-white px-5 py-2 rounded-xl font-semibold text-sm">Sign in </button> + </a> + {/if} +>>>>>>> master </nav> <div - class="absolute top-[calc(20vh-48px)] left-[calc(50vw-48px)] rounded-full border-4 border-white p-4 bg-gradient-to-r from-primary to-gradient w-24 h-24" + class="absolute top-[calc(175px-48px)] {avatarPosition} rounded-full border-4 border-white p-4 bg-gradient-to-r from-primary to-gradient w-24 h-24" > <Icon icon="logo-symbol-white" style="mx-auto" /> </div> </header> + +<style> + .z { + z-index: 100; + } +</style> diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte new file mode 100644 index 00000000..0c682179 --- /dev/null +++ b/src/components/Footer.svelte @@ -0,0 +1,39 @@ +<script> + import { Icon } from '$comp'; + + const followLinks = [ + { url: 'https://twitter.com/coinoswallet', title: 'Twitter' }, + { url: 'https://github.com/coinos', title: 'GitHub' }, + { url: 'https://t.me/coinos', title: 'Telegram' } + ]; + + const companyLinks = [ + { url: 'mailto:[email protected]', title: 'Support' }, + { url: '/community', title: 'Community' }, + { url: '/jobs', title: 'Jobs' } + ]; +</script> + +<footer class="space-y-10 md:space-y-0 text-xl block md:flex justify-around items-center mb-20"> + <Icon icon="logo" style="w-48 mx-auto md:mx-0" /> + + <div class="text-center md:text-left"> + <p class="font-semibold">Follow Us</p> + <ul class="mt-5 text-secondary space-y-3 font-medium"> + {#each followLinks as link} + <li><a href={link.url} target="_blank" rel="noreferrer">{link.title}</a></li> + {/each} + </ul> + </div> + + <div class="text-center md:text-left"> + <p class="font-semibold">Company</p> + <ul class="mt-5 text-secondary space-y-3"> + {#each companyLinks as link} + <li><a href={link.url}>{link.title}</a></li> + {/each} + </ul> + </div> + + <Icon icon="shield" style="mx-auto md:mx-0" /> +</footer> diff --git a/src/components/LandingHeader.svelte b/src/components/LandingHeader.svelte index ca9d431e..352b200c 100644 --- a/src/components/LandingHeader.svelte +++ b/src/components/LandingHeader.svelte @@ -2,34 +2,112 @@ import { goto } from '$app/navigation'; import { scroll } from '$lib/utils'; import { Icon, LocaleSelector } from '$comp'; - - import { _, locale, locales } from 'svelte-i18n'; + import { user } from '$lib/store'; + import { _ } from 'svelte-i18n'; export let howItWorks; export let faq; + export let about; + + let showMobileMenu = false; + const mobileMenuButtonClick = (section) => { + showMobileMenu = false; + scroll(section); + }; </script> -<header class="w-full lg:w-5/6 pt-10 mx-auto"> +<header + class="w-full lg:w-5/6 py-5 xl:pb-0 xl:pt-10 mx-auto fixed md:sticky z-10 top-0 bg-white/90" +> <nav class="block xl:flex flex-wrap justify-between items-center"> - <a href="/"> + <a href="/" class="hidden md:block"> <Icon icon="logo" style="mx-auto mb-5 xl:mb-0" /> </a> + <Icon icon="logo" style="block md:hidden ml-5 w-48" /> <LocaleSelector /> + <!-- desktop nav --> <div class="hidden space-x-10 md:flex flex-wrap justify-center items-center font-bold"> <button on:click={() => scroll(howItWorks)}>{$_('howItWorks')}</button> <button on:click={() => scroll(faq)}>{$_('FAQ')}</button> - <button>{$_('about')}</button> - <button class="border rounded-full px-6 py-2 font-bold" on:click={() => goto('/register')} - >{$_('startInSeconds')}</button - > - <button - class="bg-black text-white border rounded-full px-6 py-2 font-bold" - on:click={() => goto('/login')} - > - {$_('signIn')} - </button> + <button on:click={() => scroll(about)}>{$_('about')}</button> + {#if !$user} + <button class="border rounded-full px-6 py-2 font-bold" on:click={() => goto('/register')} + >{$_('startInSeconds')} + </button> + <button + class="bg-black text-white border rounded-full px-6 py-2 font-bold" + on:click={() => goto('/login')} + > + {$_('signIn')} + </button> + {:else} + <button + class="border rounded-full px-6 py-2 font-bold" + on:click={() => goto(`/${$user.username}/dashboard`)} + >Account + </button> + <button + class="bg-black text-white border rounded-full px-6 py-2 font-bold" + on:click={() => goto('/logout')} + > + Sign out + </button> + {/if} + </div> + + <!-- mobile nav --> + <button + class="block md:hidden absolute top-[34.5px] right-10 z" + on:click={() => (showMobileMenu = !showMobileMenu)} + ><Icon icon={!showMobileMenu ? 'menu' : 'close'} /> + </button> + + <div + class="block md:hidden absolute top-0 {showMobileMenu + ? 'right-0' + : 'right-[-100%]'} transition-all ease-in-out duration-300 h-[100vh] w-full bg-white" + > + <div class="space-y-8 mt-24 ml-10 font-bold text-xl"> + <button on:click={() => mobileMenuButtonClick(howItWorks)} class="block" + >{$_('howItWorks')}</button + > + <button on:click={() => mobileMenuButtonClick(faq)} class="block">{$_('FAQ')}</button> + <button on:click={() => mobileMenuButtonClick(about)} class="block">{$_('about')}</button> + {#if !$user} + <button + class="border rounded-full px-6 py-2 font-bold block" + on:click={() => goto('/register')} + >{$_('startInSeconds')} + </button> + <button + class="bg-black text-white border rounded-full px-6 py-2 font-bold block" + on:click={() => goto('/login')} + > + {$_('startInSeconds')} + </button> + {:else} + <button + class="border rounded-full px-6 py-2 font-bold block" + on:click={() => goto(`/${$user.username}/dashboard`)} + >Account + </button> + <button + class="bg-black text-white border rounded-full px-6 py-2 font-bold block" + on:click={() => goto('/logout')} + > + Sign out + </button> + {/if} + </div> +>>>>>>> master </div> </nav> </header> + +<style> + .z { + z-index: 100; + } +</style> diff --git a/src/components/LandingHero.svelte b/src/components/LandingHero.svelte index 2c2e1ab5..93bea8c2 100644 --- a/src/components/LandingHero.svelte +++ b/src/components/LandingHero.svelte @@ -1,9 +1,10 @@ <script> import { goto } from '$app/navigation'; import { Icon, Image } from '$comp'; + import { user } from '$lib/store'; </script> -<div> +<div class="mt-[96.88px] md:mt-0"> <div class="space-y-10"> <h1 class="text-5xl md:text-6xl lg:text-7xl 2xl:text-8xl font-medium text-center w-full md:w-3/4 lg:w-1/2 max-w-7xl mx-auto leading-tight" @@ -19,14 +20,16 @@ Grow your customer base and revenue by accepting Bitcoin with an easy-to-use and managed Point of Sale. No software or hardware required. </h2> - <div class="flex justify-center"> - <button - class="bg-black text-white border rounded-full px-6 py-2 font-bold" - on:click={() => goto('/register')} - > - Start in Seconds - </button> - </div> + {#if !$user} + <div class="flex justify-center"> + <button + class="bg-black text-white border rounded-full px-6 py-2 font-bold" + on:click={() => goto('/register')} + > + Start in Seconds + </button> + </div> + {/if} </div> <!-- hero image --> diff --git a/src/components/LoadingSplash.svelte b/src/components/LoadingSplash.svelte new file mode 100644 index 00000000..2ab4c739 --- /dev/null +++ b/src/components/LoadingSplash.svelte @@ -0,0 +1,7 @@ +<script> + import { Icon } from '$comp'; +</script> + +<div class="flex justify-center items-center"> + <Icon icon="logo" style="mt-[calc(50vh-72px)]" /> +</div> diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte index 7cc41709..157c2821 100644 --- a/src/components/RegisterOrLogin.svelte +++ b/src/components/RegisterOrLogin.svelte @@ -1,13 +1,15 @@ <script> + import { toast } from '@zerodevx/svelte-toast'; import { Icon } from '$comp'; import { post } from '$lib/utils'; - import { token } from '$lib/store'; - import { connect } from '$lib/socket'; + import { user, token } from '$lib/store'; + import { auth } from '$lib/socket'; import { goto } from '$app/navigation'; export let page; - let username, password; + let username = 'bob', + password = 'pw'; let revealPassword = false; @@ -16,20 +18,27 @@ 'Sign in': '/login' }[page]; - let submit = () => - post(url, { username, password }).then((r) => - r.json().then((r) => { - $token = r.token; - connect(); - goto('/receive'); - }) - ); + let submit = async () => { + try { + let r = await post(url, { username, password }); + if (!r.token) r = await post('/login', { username, password }); + $token = r.token; + auth(); + } catch (e) { + if (!e.message) e.message = 'Login failed'; + toast.push(e.message); + } + }; + + $: if ($user) goto(`/${username}/receive`); </script> <div class="pt-10"> - <a href="/"> - <Icon icon="logo" style="mx-auto mb-10" /> - </a> + <div class="w-[243px] mx-auto mb-10"> + <a href="/"> + <Icon icon="logo" /> + </a> + </div> <div class="flex justify-center items-center"> <div class="shadow-xl rounded-3xl p-10 pb-12 space-y-5 w-full mx-5 md:mx-0 md:w-[400px]"> @@ -38,11 +47,13 @@ <form class="space-y-5" on:submit|preventDefault={submit}> <div> <label for="username" class="font-semibold">Username</label> + <!-- svelte-ignore a11y-autofocus --> <input type="text" required class="bg-primary p-4 rounded-2xl w-full" bind:value={username} + autofocus /> </div> @@ -78,7 +89,9 @@ </p> {:else} <div class="flex justify-end items-center"> - <a href="" class="underline underline-offset-4 text-black text-sm">Forgot Password?</a> + <a href="/forgot" class="underline underline-offset-4 text-black text-sm" + >Forgot Password?</a + > </div> {/if} diff --git a/src/components/index.js b/src/components/index.js index f0e86183..1d5b369d 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -8,3 +8,7 @@ export { default as Image } from './Image.svelte'; export { default as RegisterOrLogin } from './RegisterOrLogin.svelte'; export { default as AppHeader } from './AppHeader.svelte'; export { default as LocaleSelector } from './LocaleSelector.svelte'; +export { default as LoadingSplash } from './LoadingSplash.svelte'; +export { default as PageNotFound } from './404.svelte'; +export { default as About } from './About.svelte'; +export { default as Footer } from './Footer.svelte'; diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 00000000..2284d88d --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,25 @@ +import cookie from 'cookie'; +import { get } from '$lib/utils'; +import { protectedRoutes } from '$lib/utils'; + +export async function handle({ event, resolve }) { + let { + params, + request: { headers } + } = event; + + const cookies = cookie.parse(headers.get('cookie') || ''); + let { token } = cookies; + event.locals = { token }; + + if (protectedRoutes.find((p) => event.url.pathname.match(p))) { + let user = await get('/me', { accept: 'application/json', authorization: `Bearer ${token}` }); + if (user.username !== params.username) throw new Error('user not found'); + } + + return resolve(event); +} + +export const getSession = ({ locals: { token } }) => { + return { token }; +}; diff --git a/src/lib/socket.js b/src/lib/socket.js index efc2651b..66091d76 100644 --- a/src/lib/socket.js +++ b/src/lib/socket.js @@ -1,23 +1,31 @@ -import { get } from "svelte/store"; -import { rate, user, token, ws } from '$lib/store'; +import { get } from 'svelte/store'; +import { rate, user, token, invoices, preferredCurrency } from '$lib/store'; let interval, socket; +export const auth = () => { + let t = get(token); + if (t) send('login', t); +}; + +export const send = (type, data) => { + socket?.readyState === 1 && socket.send(JSON.stringify({ type, data })); +}; + export const messages = (data) => ({ rate() { rate.set(data); }, payment() { - console.log(data); + invoices.set({ ...get(invoices), [data.invoice.uuid]: data.invoice }); }, - connected() { - socket.send(JSON.stringify({ type: 'login', data: { token: get(token) } })); - }, + connected: auth, login() { user.set(data); + preferredCurrency.set(data.currency); } }); @@ -29,18 +37,18 @@ let currentReconnectDelay = initialReconnectDelay; export function connect() { clearInterval(interval); - if (socket) socket.close(); + if (socket) return auth(); + socket = new WebSocket('ws://localhost:3119/ws'); socket.addEventListener('open', onWebsocketOpen); socket.addEventListener('close', onWebsocketClose); socket.addEventListener('message', onWebsocketMessage); - interval = setInterval( - () => socket.readyState === 1 && socket.send(JSON.stringify({ type: 'heartbeat' })), - 5000 - ); + interval = setInterval(() => send('heartbeat'), 5000); +} - ws.set(socket); +export function close() { + if (socket) socket.close(); } function onWebsocketMessage(msg) { @@ -53,7 +61,7 @@ function onWebsocketOpen() { } function onWebsocketClose() { - ws.set(null); + socket = null; setTimeout(() => { reconnectToWebsocket(); }, currentReconnectDelay + Math.floor(Math.random() * 3000)); diff --git a/src/lib/store.js b/src/lib/store.js index 3963f55a..b20fd10f 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -1,6 +1,22 @@ -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; +import { browser } from '$app/env'; + +const persisted = (k, i) => { + let s = writable( + browser && sessionStorage.getItem(k) && sessionStorage.getItem(k) !== 'undefined' + ? JSON.parse(sessionStorage.getItem(k)) + : i + ); + + s.subscribe((v) => browser && sessionStorage.setItem(k, JSON.stringify(v))); + + return s; +}; export const rate = writable(); export const user = writable(); -export const token = writable(); -export const ws = writable(); +export const invoiceAmount = writable(); +export const invoiceAmountFiat = writable(); +export const token = persisted('token'); +export const invoices = writable({}); +export const preferredCurrency = writable(); diff --git a/src/lib/utils.js b/src/lib/utils.js index 595d2dd8..669b2b5e 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,10 +1,53 @@ +import { browser } from '$app/env'; + export function scroll(section) { section.scrollIntoView({ behavior: 'smooth' }); } +const base = browser ? '' : import.meta.env.VITE_COINOS_URL; + +export const get = (url, headers = { accept: 'application/json' }) => + fetch(base + url, { headers }) + .then((r) => r.text()) + .then((body) => { + try { + return JSON.parse(body); + } catch (e) { + throw new Error(body); + } + }); + export const post = (url, body) => - fetch(url, { + fetch(base + url, { method: 'POST', body: JSON.stringify(body), - headers: { 'accept': 'application/json', 'content-type': 'application/json' } - }); + headers: { accept: 'application/json', 'content-type': 'application/json' } + }) + .then((r) => r.text()) + .then((body) => { + try { + body = JSON.parse(body); + } catch (e) { + throw new Error(body); + } + + if (body instanceof Error) throw body; + if (body.name === 'Error') throw new Error(body.message); + + return body; + }); + +export const copy = (text) => { + navigator.clipboard.writeText(text); +}; + +export function reverseFormat(val, locale) { + let parts = new Intl.NumberFormat(locale).formatToParts(1111.1); + let group = parts.find((part) => part.type === 'group').value; + let decimal = parts.find((part) => part.type === 'decimal').value; + let reversedVal = val.replace(new RegExp('\\' + group, 'g'), ''); + reversedVal = reversedVal.replace(new RegExp('\\' + decimal, 'g'), '.'); + return Number.isNaN(reversedVal) ? 0 : +reversedVal; +} + +export let protectedRoutes = [/receive/, /customers/, /dashboard/, /settings/, /transactions/]; diff --git a/src/routes/[username]/customers.svelte b/src/routes/[username]/customers.svelte new file mode 100644 index 00000000..1935b156 --- /dev/null +++ b/src/routes/[username]/customers.svelte @@ -0,0 +1,5 @@ +<script> + import { AppHeader } from '$comp'; +</script> + +<AppHeader /> diff --git a/src/routes/[username]/dashboard.svelte b/src/routes/[username]/dashboard.svelte new file mode 100644 index 00000000..1935b156 --- /dev/null +++ b/src/routes/[username]/dashboard.svelte @@ -0,0 +1,5 @@ +<script> + import { AppHeader } from '$comp'; +</script> + +<AppHeader /> diff --git a/src/routes/[username]/index.js b/src/routes/[username]/index.js new file mode 100644 index 00000000..5de1316f --- /dev/null +++ b/src/routes/[username]/index.js @@ -0,0 +1,14 @@ +import { get } from '$lib/utils'; + +export async function GET({ params }) { + let { username } = params; + let body; + + try { + body = await get(`/users/${username}`); + } catch (e) { + body = {}; + } + + return { body }; +} diff --git a/src/routes/[username]/index.svelte b/src/routes/[username]/index.svelte new file mode 100644 index 00000000..6002efdc --- /dev/null +++ b/src/routes/[username]/index.svelte @@ -0,0 +1,95 @@ +<script> + import { toast } from '@zerodevx/svelte-toast'; + import { browser } from '$app/env'; + import { onMount } from 'svelte'; + import { AppHeader, Icon, PageNotFound } from '$comp'; + import { user } from '$lib/store'; + import { copy } from '$lib/utils'; + export let username; + + let lightningAddress = `${username}@coinos.io`; + + onMount(async () => { + if (browser) { + let { default: QRCodeStyling } = await import('qr-code-styling'); + const qrCode = new QRCodeStyling({ + width: window.screen.width < 640 ? 250 : 300, + type: 'svg', + data: 'add static qr here', + image: '/images/invoice.svg', + backgroundOptions: { + color: 'rgba(0, 0, 0, 0)' + }, + dotsOptions: { + type: 'rounded' + }, + imageOptions: { + hideBackgroundDots: false + } + }); + + qrCode.append(document.getElementById('qr')); + } + }); + + let followed = false; + + let qrSkeleton = true; + setTimeout(() => (qrSkeleton = false), 100); + + const handleCopy = () => { + copy(lightningAddress); + toast.push('Copied!', { + theme: { + '--toastBarBackground': '#2F855A' + } + }); + }; +</script> + +{#if username} + <AppHeader avatarPosition="left-[calc(15vw-48px)]" /> + + <div class="flex xl:block justify-center items-center my-20 px-3"> + <div class="space-y-10 xl:space-y-0"> + <div class="xl:ml-[calc(15vw-48px)] space-y-2 w-full md:w-72"> + <h1 class="text-3xl font-bold">{username}</h1> + + <p class="text-secondary">Address goes here</p> + + <button + class="border rounded-full py-2 w-28 font-semibold flex justify-center items-center text-sm" + on:click={() => (followed = !followed)} + ><Icon icon={followed ? 'minus' : 'plus'} style="mr-1" /> + {followed ? 'Remove' : 'Follow'} + </button> + </div> + + <div class="space-y-5 static xl:absolute top-[255px] left-[calc(50vw-150px)]"> + <div + id="qr" + class="border border-lightgrey rounded-3xl flex md:p-5 justify-center items-center relative" + > + <div + class="z-100 h-[300px] md:h-[342px] w-[250px] md:w-[342px] animate-pulse absolute top-0 left-0 bg-gray-400 rounded-3xl {qrSkeleton + ? 'block' + : 'hidden'}" + /> + </div> + + <p class="text-secondary text-center font-semibold">Scan to pay with Bitcoin</p> + + <div + class="bg-primary font-semibold rounded-xl text-sm py-2 px-3 flex flex-wrap justify-center items-center" + > + <span>{lightningAddress}</span> + <button on:click={handleCopy}> + <Icon icon="copy" style="ml-2" /> + </button> + </div> + </div> + </div> + </div> +{:else} + <PageNotFound /> +{/if} diff --git a/src/routes/[username]/receive.svelte b/src/routes/[username]/receive.svelte new file mode 100644 index 00000000..50fddbed --- /dev/null +++ b/src/routes/[username]/receive.svelte @@ -0,0 +1,187 @@ +<script> + // in the future we will want to allow users to set their fiat currency in the settings + import { AppHeader, Icon } from '$comp'; + import { goto } from '$app/navigation'; + import { invoiceAmount, invoiceAmountFiat, rate, user } from '$lib/store'; + import { post } from '$lib/utils'; + import { page } from '$app/stores'; + + let useFiat = true; + let amountFiat = 0; + let amountSats = 0; + let message = ''; + + $: amount = useFiat ? Math.round(amountFiat / ($rate / 100000000)) : amountSats; + + $: $invoiceAmount = parseInt(amountSats === 0 ? amountFiat / ($rate / 100000000) : amountSats); + $: $invoiceAmountFiat = parseInt( + amountFiat === 0 ? amountSats * ($rate / 100000000) : amountFiat + ); + $: amountSatsFormatted = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + amountSats + ); + + $: amountFiatConverted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amountSats * ($rate / 100000000)); + + $: amountSatsConverted = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + amountFiat / ($rate / 100000000) + ); + + const numPad = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', '<']; + + const handleInput = (value) => { + if (message) { + message = ''; + } + if (useFiat) { + if (amountFiat === 0 && value !== '.' && value !== '<' && value !== '0') { + amountFiat = value; + } else if ((amountFiat === 0 || amountFiat === '0') && value === '0') { + return; + } else if (amountFiat === 0 && value === '.') { + amountFiat = '0.'; + } else if (amountFiat !== 0 && amountFiat.includes('.') && value === '.') { + return; + } else if (value === '<') { + if (amountFiat !== 0) { + amountFiat = amountFiat.slice(0, amountFiat.length - 1); + if (amountFiat.length === 0) { + amountFiat = 0; + amountSats = 0; + } + } + } else if (value !== '.' && value !== '<' && parseInt(amountFiat + value) > $rate) { + message = 'Please enter an amount less than 1 bitcoin.'; + } else if (amountFiat !== 0 && amountFiat.match(/\.../)) { + return; + } else { + amountFiat = amountFiat + value; + } + } else { + if (value === '.') { + return; + } else if (amountSats === 0 && value !== '<' && value !== '0') { + amountSats = value; + } else if (value === '<') { + if (amountSats !== 0) { + amountSats = amountSats.slice(0, amountSats.length - 1); + if (amountSats.length === 0) { + amountSats = 0; + amountFiat = 0; + } + } + } else if (value !== '<' && parseInt(amountSats + value) > 100000000) { + message = 'Please enter an amount less than 1 bitcoin.'; + } else { + amountSats = amountSats + value; + } + } + }; + + let submit = async () => { + let { id } = await post('/invoice', { amount, rate: $rate, username: $page.params.username }); + goto(`/invoice/${id}`); + }; +</script> + +{#if $user} + <AppHeader /> + + <div class="flex justify-center items-center mt-20 mb-3 px-3"> + {#if $rate} + <div class="space-y-5"> + <!-- amounts --> + <div class="text-center"> + <div class="text-5xl md:text-6xl font-semibold tracking-widest mb-1"> + {useFiat ? `$${amountFiat}` : amountSatsFormatted}<span + class="tracking-normal text-base font-normal">{useFiat ? 'USD' : 'SATS'}</span + > + </div> + <span class="text-secondary mr-1" + >{useFiat ? `${amountSatsConverted} SATS` : `${amountFiatConverted} USD`}</span + > + <button + on:click={() => { + if (useFiat) { + amountSats = (amountFiat / ($rate / 100000000)).toFixed(0).toString(); + } else { + amountFiat = + (amountSats * ($rate / 100000000)).toFixed(2) > 0.0 + ? (amountSats * ($rate / 100000000)).toFixed(2).toString() + : 0; + } + useFiat = !useFiat; + }}><Icon icon="swap" style="inline" /></button + > + </div> + + <!-- error message --> + {#if message} + <div + class="text-orange-500 text-sm text-center font-bold bg-primary p-2 rounded-xl flex justify-center items-center space-x-1" + > + <Icon icon="info" style="w-5" /> + <p>{message}</p> + </div> + {/if} + + <!-- numpad --> + <div class="grid grid-cols-3 gap-2 w-full md:w-[300px] mx-auto"> + {#each numPad as value} + {#if value === '<'} + <button + class="bg-primary rounded-xl py-4 px-8 font-semibold active:bg-black active:text-white flex justify-center items-center" + on:click={() => handleInput(value)} + > + <svg + class="w-5" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M15 19L8 12L15 5" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </button> + {:else} + <button + class="bg-primary rounded-xl py-4 px-8 font-semibold active:bg-black active:text-white" + on:click={() => handleInput(value)}>{value}</button + > + {/if} + {/each} + </div> + + <!-- buttons --> + <div class="space-y-3 w-full md:w-[300px] mx-auto"> + <button + class="bg-black text-white rounded-xl w-full h-[48px] flex justify-center items-center font-semibold text-sm {$invoiceAmount === + 0 + ? 'opacity-50' + : 'opacity-100'}" + on:click={submit} + disabled={$invoiceAmount === 0}><Icon icon="qr" style="mr-2" /> Show QR</button + > + <button + class="bg-primary rounded-xl w-full h-[48px] flex justify-center items-center font-semibold text-sm" + disabled={true}><Icon icon="send" style="mr-2" /> Send Invoice</button + > + </div> + </div> + {:else} + <div + class="w-full md:w-[300px] h-[475px] md:h-[485px] animate-pulse bg-gray-400 rounded-xl" + /> + {/if} + </div> +{/if} diff --git a/src/routes/[username]/settings.svelte b/src/routes/[username]/settings.svelte new file mode 100644 index 00000000..1935b156 --- /dev/null +++ b/src/routes/[username]/settings.svelte @@ -0,0 +1,5 @@ +<script> + import { AppHeader } from '$comp'; +</script> + +<AppHeader /> diff --git a/src/routes/[username]/transactions.svelte b/src/routes/[username]/transactions.svelte new file mode 100644 index 00000000..1935b156 --- /dev/null +++ b/src/routes/[username]/transactions.svelte @@ -0,0 +1,5 @@ +<script> + import { AppHeader } from '$comp'; +</script> + +<AppHeader /> diff --git a/src/routes/__error.svelte b/src/routes/__error.svelte new file mode 100644 index 00000000..2d617f9d --- /dev/null +++ b/src/routes/__error.svelte @@ -0,0 +1,38 @@ +<script context="module"> + export function load({ error, status }) { + if (error.message.includes('not found')) { + error.message = 'Page not found'; + status = 404; + } + + return { + props: { + error, + status + } + }; + } +</script> + +<script> + import { Icon, PageNotFound } from '$comp'; + export let status, error; +</script> + +{#if status === 404} + <PageNotFound /> +{:else} + <div class="flex justify-center items-center mt-20"> + <div class="space-y-5 text-center"> + <a href="/"> + <Icon icon="logo" /> + </a> + + <Icon icon="error" style="mx-auto w-full" /> + + <h1 class="font-bold text-5xl">Error</h1> + <p>{error.message}</p> + <p>Please try again or contact support.</p> + </div> + </div> +{/if} diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index ed1ad188..9ec36c57 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -1,11 +1,47 @@ <script> + import { SvelteToast } from '@zerodevx/svelte-toast'; + import { toast } from '@zerodevx/svelte-toast'; import '../app.css'; + import '$lib/i18n'; import { onMount } from 'svelte'; import { connect } from '$lib/socket'; + import { user, token } from '$lib/store'; + import { page, session } from '$app/stores'; + import { browser } from '$app/env'; + import { goto } from '$app/navigation'; + import { LoadingSplash } from '$comp'; + import { protectedRoutes } from '$lib/utils'; - import '$lib/i18n'; + browser && ($token ||= $session.token); + let ready = false; - onMount(connect); + onMount(() => { + if (protectedRoutes.find((p) => $page.url.pathname.match(p))) { + if (!$token) { + goto('/login'); + toast.push('Please sign in to continue.'); + } + user.subscribe((u) => u?.username && (ready = true)); + } else { + ready = true; + } + + connect(); + }); </script> -<slot /> +{#if ready} + <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> + <slot /> +{:else} + <LoadingSplash /> +{/if} + +<style> + :root { + --toastContainerTop: auto; + --toastContainerRight: auto; + --toastContainerBottom: 8rem; + --toastContainerLeft: calc(50vw - 8rem); + } +</style> diff --git a/src/routes/community.svelte b/src/routes/community.svelte new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/routes/community.svelte diff --git a/src/routes/index.svelte b/src/routes/index.svelte index cda885a3..faa881bb 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -1,5 +1,14 @@ <script> - import { LandingHeader, LandingHero, LandingInfoCard, HowItWorksCard, FaqCard } from '$comp'; + import { + LandingHeader, + LandingHero, + LandingInfoCard, + HowItWorksCard, + FaqCard, + Image, + About, + Footer + } from '$comp'; let howItWorks; let faq; @@ -42,9 +51,9 @@ ]; </script> -<LandingHeader {howItWorks} {faq} /> +<LandingHeader {howItWorks} {faq} {about} /> -<main class="space-y-40 py-20 md:py-32 lg:py-40 px-5 md:px-0"> +<main class="space-y-40 py-20 md:py-32 lg:py-36 xl:py-40 px-5 md:px-0"> <!-- hero --> <LandingHero /> @@ -73,15 +82,18 @@ </LandingInfoCard> <!-- landing info 3 --> - <LandingInfoCard - image="customize" - title="Brand it your way!" - description="Customize your point of sale look and feel, or keep it on default - totally up to you." - /> + <div> + <LandingInfoCard + image="customize" + title="Brand it your way!" + description="Customize your point of sale look and feel, or keep it on default - totally up to you." + /> + <div bind:this={howItWorks} /> + </div> <!-- how it works --> - <div bind:this={howItWorks}> - <h3 class="text-5xl font-medium mb-20 text-center">How it works</h3> + <div> + <h3 class="text-5xl font-medium mb-10 text-center">How it works</h3> <div class="block lg:flex justify-center items-center space-y-10 lg:space-y-0 lg:space-x-10 text-center" > @@ -89,13 +101,24 @@ <HowItWorksCard image={step.image} step={step.step} description={step.description} /> {/each} </div> + <div bind:this={faq} /> </div> <!-- faq --> - <div class="space-y-10" bind:this={faq}> - <h3 class="text-5xl font-medium text-center">FAQ</h3> - {#each faqQuestions as question} - <FaqCard question={question.question} answer={question.answer} /> - {/each} + <div> + <div class="space-y-10"> + <h3 class="text-5xl font-medium text-center">FAQ</h3> + {#each faqQuestions as question} + <FaqCard question={question.question} answer={question.answer} /> + {/each} + </div> + <div bind:this={about} /> + </div> + + <!-- about --> + <div> + <About /> </div> </main> + +<Footer /> diff --git a/src/routes/invoice.js b/src/routes/invoice.js deleted file mode 100644 index 49bcfe70..00000000 --- a/src/routes/invoice.js +++ /dev/null @@ -1,20 +0,0 @@ -import got from 'got'; - -const username = 'bob', - network = 'lightning'; - -export async function GET({ params }) { - let text = await got - .post('http://localhost:3119/lightning/invoice', { - json: { amount: 100 } - }) - .text(); - - await got - .post('http://localhost:3119/invoice', { - json: { invoice: { text, network }, user: { username } } - }) - .json(); - - return { body: { text } }; -} diff --git a/src/routes/invoice.svelte b/src/routes/invoice.svelte deleted file mode 100644 index b8f9e54b..00000000 --- a/src/routes/invoice.svelte +++ /dev/null @@ -1,18 +0,0 @@ -<script> - import { rate, user } from '$lib/store'; - import { onMount } from 'svelte'; - - export let text; -</script> - -QR code - -{text} - -{$rate} - -{$user?.username} - -{#each $user.payments as p} - {p.amount}<br /> -{/each} diff --git a/src/routes/invoice/[id].js b/src/routes/invoice/[id].js new file mode 100644 index 00000000..a58d02a1 --- /dev/null +++ b/src/routes/invoice/[id].js @@ -0,0 +1,14 @@ +import { get } from '$lib/utils'; + +export async function GET({ params }) { + let { id } = params; + let { + amount, + rate, + status, + text, + user: { username } + } = await get(`/invoice?uuid=${id}`); + + return { body: { amount, id, rate, username, status, text } }; +} diff --git a/src/routes/invoice/[id].svelte b/src/routes/invoice/[id].svelte new file mode 100644 index 00000000..17c7de1f --- /dev/null +++ b/src/routes/invoice/[id].svelte @@ -0,0 +1,357 @@ +<script> + import { toast } from '@zerodevx/svelte-toast'; + import { get, post, reverseFormat } from '$lib/utils'; + import { browser } from '$app/env'; + import { invoices, user, preferredCurrency } from '$lib/store'; + import { Icon, Image } from '$comp'; + import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; + import { copy } from '$lib/utils'; + + export let amount, id, rate, status, text, username; + + $invoices[id] = { amount, id, rate, status, text, username }; + + $: amountFiat = parseFloat(((amount * rate) / 100000000).toFixed(2)); + + let tipPercent = 0; + let qr; + + $: tipAmount = customTipAmount + ? customTipAmount < 0 + ? (customTipAmount = 0) + : customTipAmount + : (amountFiat / 100) * tipPercent; + + $: tipAmountFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(tipAmount); + + $: tipAmountSats = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + tipAmount / (rate / 100000000) + ); + + $: totalAmountSats = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + amount + parseFloat(tipAmountSats.replace(/,/g, '')) + ); + + $: update(tipAmountSats); + let update = async (n) => { + let { id } = await post('/invoice', { + amount: amount + reverseFormat(n), + rate, + text, + username + }); + ({ text } = await get(`/invoice/${id}`)); + }; + + $: totalAmountFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amountFiat + parseFloat(tipAmount)); + + const amountFormatted = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 0 + }).format(amount); + + $: invoiceAmountFiatFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amountFiat); + + let customTipAmount; + + const tipAmounts = ['None', '10%', '15%', '20%']; + + const handleTipButtonClick = (amount) => { + customTipAmount = ''; + customInput.value = ''; + + if (amount === 'None') { + tipPercent = 0; + } else { + tipPercent = amount.slice(0, 2); + } + }; + + const handleCustomTipAmount = (e) => { + tipPercent = 0; + customTipAmount = e.target.value; + }; + + $: generate(text); + let generate = async (ready) => { + console.log('GENERATE', text); + if (!ready) return; + let { default: QRCodeStyling } = await import('qr-code-styling'); + + const qrCode = new QRCodeStyling({ + width: window.screen.width < 640 ? 250 : 300, + type: 'canvas', + data: text, + image: '/images/invoice.svg', + backgroundOptions: { + color: 'rgba(0, 0, 0, 0)' + }, + dotsOptions: { + type: 'rounded' + }, + imageOptions: { + hideBackgroundDots: false + } + }); + + qrCode.append(qr); + }; + + let showMobileTip = false; + + const handleBackButton = () => { + if (showMobileTip) { + tipPercent = 0; + customInput.value = ''; + customTipAmount = 0; + tipAmount = 0; + showMobileTip = false; + } else { + goto(`/${$user.username}/receive`); + } + }; + + let customInput; + + const handleCopy = () => { + copy(text); + toast.push('Copied!', { + theme: { + '--toastBarBackground': '#2F855A' + } + }); + }; + + const handleDoneClick = () => { + if ($user) { + goto(`/${$user.username}/receive`); + } else { + goto('/'); + } + }; +</script> + +{#if $invoices[id]?.status !== 'paid'} + <div class:full-shadow={showMobileTip}> + <button + class="ml-5 md:ml-20 mt-5 md:mt-10" + class:invisible={!$user} + on:click={handleBackButton} + > + <Icon icon="arrow-left" style="w-10" /> + </button> + + <div class="flex justify-center items-center mb-20 md:mt-[20px] px-3"> + <div + class="block lg:flex justify-center items-center space-y-10 lg:space-y-0 space-x-0 lg:space-x-24" + > + <!-- tipping section --> + <div + class="w-[100%] md:w-auto absolute {showMobileTip + ? 'bottom-0' + : '-bottom-full'} bg-white p-6 md:p-0 rounded-t-3xl md:rounded-none transition-all md:transition-none ease-in-out duration-500 left-0 md:static text-center space-y-5" + > + <h1 class="hidden md:block text-4xl font-semibold">Add a tip?</h1> + + <div> + <span class="font-semibold mr-1">{tipAmountFormatted}</span><span + class="text-secondary font-semibold text-sm">{`(${tipAmountSats} SATS)`}</span + > + <span class="block text-secondary text-sm">{tipPercent}%</span> + </div> + + <input + id="slider" + type="range" + min="0" + max="100" + value={tipPercent} + class="px-2" + on:input={(e) => { + customTipAmount = ''; + customInput.value = ''; + tipPercent = e.target.value; + }} + /> + + <div> + {#each tipAmounts as amount} + <button + class="bg-primary w-16 md:w-20 py-3 m-1 rounded-2xl font-semibold {customTipAmount + ? '' + : tipPercent === amount.slice(0, 2) || + ((tipPercent === 0 || tipPercent === '0') && amount === 'None') + ? '!bg-black text-white' + : ''}" + on:click={() => handleTipButtonClick(amount)}>{amount}</button + > + {/each} + + <div class="relative"> + <span + class="absolute top-[20px] left-5 font-semibold {!customTipAmount + ? 'opacity-50' + : 'opacity-100'}">$</span + > + <input + bind:this={customInput} + type="number" + min="0" + on:input={(e) => handleCustomTipAmount(e)} + class="bg-primary rounded-2xl py-3 px-10 font-semibold mt-2 mr-1 w-full md:w-auto" + placeholder="Other" + /> + <button + class="w-full md:w-auto mt-2 md:mt-0 bg-black text-white font-semibold py-3 px-7 rounded-2xl {showMobileTip + ? !customTipAmount && !tipAmount + ? 'opacity-50' + : 'opacity-100' + : !customTipAmount + ? 'opacity-50' + : 'opacity-100'}" + disabled={showMobileTip ? !customTipAmount && !tipAmount : !customTipAmount} + on:click={() => (showMobileTip = false)}>Done</button + > + </div> + </div> + </div> + + <div + class="hidden md:block h-[1px] lg:h-[600px] w-full lg:w-[1px] bg-lightgrey rounded-xl" + id="section-divider" + /> + + <!-- invoice section --> + <div class="text-center space-y-5"> + <button + class="block md:hidden bg-black {showMobileTip + ? 'text-white/50' + : 'text-white'} rounded-xl px-6 py-3 block text-sm font-semibold mx-auto flex justify-center items-center" + on:click={() => (showMobileTip = true)} + disabled={showMobileTip} + > + <svg + width="24" + height="24" + class="mr-1" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4.31802 6.31802C2.56066 8.07538 2.56066 10.9246 4.31802 12.682L12.0001 20.364L19.682 12.682C21.4393 10.9246 21.4393 8.07538 19.682 6.31802C17.9246 4.56066 15.0754 4.56066 13.318 6.31802L12.0001 7.63609L10.682 6.31802C8.92462 4.56066 6.07538 4.56066 4.31802 6.31802Z" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> Add a tip + </button> + + <span class="text-secondary block" + >Scan to pay <span class="text-black font-semibold">{username}</span></span + > + + <div + bind:this={qr} + on:click={handleCopy} + class="border {showMobileTip + ? 'border-gray-400' + : 'border-lightgrey'} rounded-3xl block md:flex justify-center items-center cursor-pointer" + /> + + <div class="px-5 space-y-3"> + <div class="flex justify-between"> + <span class="font-semibold text-sm">Invoice</span> + <span class="font-semibold text-sm" + >{invoiceAmountFiatFormatted} + <span class="text-secondary font-normal">{`(${amountFormatted} SATS)`}</span></span + > + </div> + + <div class="flex justify-between"> + <span class="font-semibold text-sm">Tip</span> + <span class="font-semibold text-sm" + >{tipAmountFormatted} + <span class="text-secondary font-normal">{`(${tipAmountSats} SATS)`}</span></span + > + </div> + + <div class="flex justify-between"> + <span class="font-bold">Total</span> + <span class="font-bold" + >{totalAmountFormatted} + <span class="text-secondary font-normal">{`(${totalAmountSats} SATS)`}</span></span + > + </div> + </div> + </div> + </div> + </div> + </div> +{:else} + <div class="text-center mt-20 md:mt-0"> + <Image image="success" style="w-full md:w-3/4 xl:w-1/2 mx-auto max-w-3xl" /> + <h1 class="text-3xl md:text-4xl font-bold mb-6">Payment Successful!</h1> + <h2 class="text-2xl md:text-3xl font-semibold">{totalAmountFormatted} {$preferredCurrency}</h2> + <h3 class="text-secondary md:text-lg mb-6 mt-1">({totalAmountSats} SATS)</h3> + <button class="bg-black text-white rounded-2xl w-20 py-3 font-bold" on:click={handleDoneClick}> + Done + </button> + </div> +{/if} + +<style> + #slider { + -webkit-appearance: none; + width: 100%; + height: 6px; + border-radius: 5px; + background: #eaecef; + outline: none; + } + + #slider:active { + background-color: black; + } + + #slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: black; + cursor: pointer; + } + + #slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: black; + cursor: pointer; + } + + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + } + + input[type='number'] { + -moz-appearance: textfield; + } + + .full-shadow { + box-shadow: inset 0 0 0 1000px rgba(0, 0, 0, 0.25); + } +</style> diff --git a/src/routes/invoice/index.js b/src/routes/invoice/index.js new file mode 100644 index 00000000..cf26b3c6 --- /dev/null +++ b/src/routes/invoice/index.js @@ -0,0 +1,14 @@ +import { post } from '$lib/utils'; + +let network = 'lightning'; + +export async function POST({ request }) { + let { amount, rate, username, text } = await request.json(); + if (!text) ({ text } = await post('/lightning/invoice', { amount })); + let { uuid: id } = await post('/invoice', { + invoice: { amount, currency: 'USD', network, rate, text }, + user: { username } + }); + + return { body: { id } }; +} diff --git a/src/routes/jobs.svelte b/src/routes/jobs.svelte new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/routes/jobs.svelte diff --git a/src/routes/login.js b/src/routes/login.js index e8c74365..db1fbb6c 100644 --- a/src/routes/login.js +++ b/src/routes/login.js @@ -1,13 +1,24 @@ -import got from 'got'; +import { post } from '$lib/utils'; +import cookie from 'cookie'; + +let maxAge = 30 * 24 * 60 * 60; export async function POST({ request }) { - let json = await request.json(); + let body = await post('/login', await request.json()); - let body = await got - .post('http://localhost:3119/login', { - json - }) - .json(); + let expires = new Date(); + expires.setSeconds(expires.getSeconds() + maxAge); - return { body }; + return { + body, + headers: { + 'set-cookie': cookie.serialize('token', body.token, { + httpOnly: true, + maxAge, + sameSite: 'lax', + path: '/', + expires + }) + } + }; } diff --git a/src/routes/logout/done.svelte b/src/routes/logout/done.svelte new file mode 100644 index 00000000..fe526357 --- /dev/null +++ b/src/routes/logout/done.svelte @@ -0,0 +1,13 @@ +<script> + import { onMount } from 'svelte'; + import { user, token } from '$lib/store'; + import { goto } from '$app/navigation'; + import { close } from '$lib/socket'; + + onMount(() => { + $token = null; + $user = null; + close(); + }); + goto('/'); +</script> diff --git a/src/routes/logout/index.js b/src/routes/logout/index.js new file mode 100644 index 00000000..6cbae8f2 --- /dev/null +++ b/src/routes/logout/index.js @@ -0,0 +1,26 @@ +import cookie from 'cookie'; + +const opts = { + httpOnly: true, + sameSite: 'lax', + path: '/' +}; + +export async function GET({ request: { headers } }) { + return { + status: 302, + headers: { + location: '/logout/done', + 'set-cookie': [ + cookie.serialize('token', '', { + ...opts, + expires: new Date(0) + }), + cookie.serialize('refresh_token', '', { + ...opts, + expires: new Date(0) + }) + ] + } + }; +} diff --git a/src/routes/logout/index.svelte b/src/routes/logout/index.svelte new file mode 100644 index 00000000..2c6a7085 --- /dev/null +++ b/src/routes/logout/index.svelte @@ -0,0 +1,10 @@ +<script> + import { onMount } from 'svelte'; + import { browser } from '$app/env'; + import { user, token } from '$lib/store'; + + onMount(() => { + $user = null; + $token = null; + }); +</script> diff --git a/src/routes/receive.svelte b/src/routes/receive.svelte deleted file mode 100644 index 73d3cc06..00000000 --- a/src/routes/receive.svelte +++ /dev/null @@ -1,75 +0,0 @@ -<script> - // in the future we will want to allow users to set their fiat currency in the settings - import { AppHeader, Icon } from '$comp'; - import { goto } from '$app/navigation'; - - let useFiat = true; - let amountFiat = 0; - let amountSats = 0; - - const numPad = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', '<']; -</script> - -<AppHeader /> - -<!-- step 1 input amount and generate QR code --> -<div class="flex justify-center items-center mt-20 mb-3 px-3"> - <div class="space-y-5"> - <!-- amounts --> - <div class="text-center"> - <div class="text-6xl font-semibold tracking-widest mb-1"> - {useFiat ? `$${amountFiat}` : amountSats}<span class="tracking-normal text-base font-normal" - >{useFiat ? 'CAD' : 'SATS'}</span - > - </div> - <span class="text-secondary mr-1" - >{useFiat ? `${amountSats} SATS` : `$${amountFiat} CAD`}</span - > <button on:click={() => (useFiat = !useFiat)}><Icon icon="swap" style="inline" /></button> - </div> - - <!-- numpad --> - <div class="grid grid-cols-3 gap-2"> - {#each numPad as value} - {#if value === '<'} - <button - class="bg-primary rounded-xl py-4 px-8 font-semibold active:bg-black active:text-white" - > - <svg - class="w-5" - width="24" - height="24" - viewBox="0 0 24 24" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M15 19L8 12L15 5" - stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" - /> - </svg></button - > - {:else} - <button - class="bg-primary rounded-xl py-4 px-8 font-semibold active:bg-black active:text-white" - >{value}</button - > - {/if} - {/each} - </div> - - <!-- buttons --> - <div class="space-y-3"> - <button - class="bg-black text-white rounded-xl w-full h-[48px] flex justify-center items-center font-semibold text-sm" - on:click={() => goto('/invoice')}><Icon icon="qr" style="mr-2" /> Show QR</button - > - <button - class="bg-primary rounded-xl w-full h-[48px] flex justify-center items-center font-semibold text-sm" - ><Icon icon="send" style="mr-2" /> Send Invoice</button - > - </div> - </div> -</div> diff --git a/src/routes/register.js b/src/routes/register.js new file mode 100644 index 00000000..1bb03ba6 --- /dev/null +++ b/src/routes/register.js @@ -0,0 +1,14 @@ +import { post } from '$lib/utils'; + +export async function POST({ request }) { + try { + let user = await request.json(); + let body = await post('/register', { user }); + return { body }; + } catch (e) { + return { + status: 500, + body: e + }; + } +} diff --git a/src/routes/register.svelte b/src/routes/register.svelte index 60a81ced..69ec5fd2 100644 --- a/src/routes/register.svelte +++ b/src/routes/register.svelte @@ -1,6 +1,5 @@ <script> import { RegisterOrLogin } from '$comp'; - import { post } from "$lib/utils"; </script> <RegisterOrLogin page="Register" /> diff --git a/src/routes/support.svelte b/src/routes/support.svelte new file mode 100644 index 00000000..1935b156 --- /dev/null +++ b/src/routes/support.svelte @@ -0,0 +1,5 @@ +<script> + import { AppHeader } from '$comp'; +</script> + +<AppHeader /> |
