diff options
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 18 | ||||
| -rw-r--r-- | src/components/RegisterOrLogin.svelte | 7 | ||||
| -rw-r--r-- | src/hooks/index.js | 18 | ||||
| -rw-r--r-- | src/lib/store.js | 4 | ||||
| -rw-r--r-- | src/routes/__layout.svelte | 8 | ||||
| -rw-r--r-- | src/routes/invoice.svelte | 253 | ||||
| -rw-r--r-- | src/routes/receive.svelte | 22 | ||||
| -rw-r--r-- | static/icons/arrow-left.svg | 3 | ||||
| -rw-r--r-- | static/images/invoice.svg | 4 | ||||
| -rw-r--r-- | tailwind.config.cjs | 3 |
11 files changed, 298 insertions, 43 deletions
diff --git a/package.json b/package.json index 23bb6cb6..d4312d1c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "postcss": "^8.4.14", "prettier": "^2.7.1", "prettier-plugin-svelte": "^2.7.0", + "qr-code-styling": "1.6.0-rc.1", "svelte": "^3.49.0", "svelte-preprocess": "^4.10.7", "tailwindcss": "^3.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c34207f..69b6d84e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,7 @@ specifiers: postcss: ^8.4.14 prettier: ^2.7.1 prettier-plugin-svelte: ^2.7.0 + qr-code-styling: 1.6.0-rc.1 svelte: ^3.49.0 svelte-preprocess: ^4.10.7 tailwindcss: ^3.1.6 @@ -15,12 +16,13 @@ specifiers: devDependencies: '@sveltejs/adapter-auto': 1.0.0-next.64 - '@sveltejs/kit': [email protected][email protected] + '@sveltejs/kit': [email protected][email protected] autoprefixer: [email protected] cookie: 0.5.0 postcss: 8.4.14 prettier: 2.7.1 prettier-plugin-svelte: 2.7.0_o3ioganyptcsrh6x4hnxvjkpqi + qr-code-styling: 1.6.0-rc.1 svelte: 3.49.0 svelte-preprocess: 4.10.7_nxvsp6sjiltnatqa6jdm4mr6zu tailwindcss: 3.1.6 @@ -121,8 +123,8 @@ packages: - supports-color dev: true - /@sveltejs/kit/[email protected][email protected]: - resolution: {integrity: sha512-bKFpuzp9QxPkOIOEIeNeedvxEMORNqBPxUmoJXDP/Se7MrSfcxYiamjBcKrG+bgGNWmV39nD3EvUox+CXno/Ig==} + /@sveltejs/kit/[email protected][email protected]: + resolution: {integrity: sha512-ONjHoRurnFUe+0lv0P9rZQpDuuJy4Cr/lDneoG+lkOR5/7gcvd03TlVdxLz1ZQAWPbgovm/PpshOvIUeC4n62A==} engines: {node: '>=16.9'} hasBin: true peerDependencies: @@ -1130,6 +1132,16 @@ packages: hasBin: true dev: true + /qr-code-styling/1.6.0-rc.1: + resolution: {integrity: sha512-ModRIiW6oUnsP18QzrRYZSc/CFKFKIdj7pUs57AEVH20ajlglRpN3HukjHk0UbNMTlKGuaYl7Gt6/O5Gg2NU2Q==} + dependencies: + qrcode-generator: 1.4.4 + dev: true + + /qrcode-generator/1.4.4: + resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==} + dev: true + /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte index 2334741d..15be60cf 100644 --- a/src/components/RegisterOrLogin.svelte +++ b/src/components/RegisterOrLogin.svelte @@ -7,7 +7,8 @@ export let page; - let username = "bob", password = "pw"; + let username = 'bob', + password = 'pw'; let revealPassword = false; @@ -20,7 +21,7 @@ let r = await post(url, { username, password }); if (!r.token) r = await post('/login', { username, password }); $token = r.token; - console.log("new token", r.token) + console.log('new token', r.token); auth(); goto('/receive'); }; @@ -43,7 +44,7 @@ required class="bg-primary p-4 rounded-2xl w-full" bind:value={username} - autofocus + autofocus /> </div> diff --git a/src/hooks/index.js b/src/hooks/index.js index 21e56052..6a9c8ec5 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,16 +1,16 @@ -import cookie from "cookie"; +import cookie from 'cookie'; export async function handle({ event, resolve }) { - let { request: { headers }, - } = event; + let { + request: { headers } + } = event; - const cookies = cookie.parse(headers.get("cookie") || ""); - let { token } = cookies; - event.locals = { token }; + const cookies = cookie.parse(headers.get('cookie') || ''); + let { token } = cookies; + event.locals = { token }; - return resolve(event); + return resolve(event); } - export const getSession = ({ locals: { token } }) => { - return { token }; + return { token }; }; diff --git a/src/lib/store.js b/src/lib/store.js index 62232f9d..6f4a9515 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -8,8 +8,7 @@ const persisted = (k, i) => { : i ); - s.subscribe((v) => browser && sessionStorage.setItem(k, JSON.stringify(v)) - ); + s.subscribe((v) => browser && sessionStorage.setItem(k, JSON.stringify(v))); return s; }; @@ -17,4 +16,5 @@ const persisted = (k, i) => { export const rate = writable(); export const user = writable(); export const invoiceAmount = writable(); +export const invoiceAmountFiat = writable(); export const token = persisted('token'); diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index 1af9679a..6562db18 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -2,11 +2,11 @@ import '../app.css'; import { onMount } from 'svelte'; import { connect } from '$lib/socket'; - import { token } from "$lib/store"; - import { session } from "$app/stores"; - import { browser } from "$app/env"; + import { token } from '$lib/store'; + import { session } from '$app/stores'; + import { browser } from '$app/env'; - browser && ($token ||= $session.token); + browser && ($token ||= $session.token); onMount(connect); </script> diff --git a/src/routes/invoice.svelte b/src/routes/invoice.svelte index 567aefbb..6dce84fe 100644 --- a/src/routes/invoice.svelte +++ b/src/routes/invoice.svelte @@ -1,15 +1,248 @@ <script> - import { rate, user, invoiceAmount } from '$lib/store'; + import { rate, user, invoiceAmount, invoiceAmountFiat } from '$lib/store'; + import { Icon } from '$comp'; + import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; + import QRCodeStyling from 'qr-code-styling'; export let text; + + let tipPercent = 0; + const setRate = ($invoiceAmountFiat / $invoiceAmount) * 100000000; + + $: tipAmount = customTipAmount + ? customTipAmount < 0 + ? (customTipAmount = 0) + : customTipAmount + : ($invoiceAmountFiat / 100) * tipPercent; + + $: tipAmountFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(tipAmount); + + $: tipAmountSats = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + tipAmount / (setRate / 100000000) + ); + + $: totalAmountSats = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + $invoiceAmount + parseFloat(tipAmountSats.replace(/,/g, '')) + ); + + $: totalAmountFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format($invoiceAmountFiat + tipAmount); + + const invoiceAmountFormatted = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 0 + }).format($invoiceAmount); + + const invoiceAmountFiatFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format($invoiceAmountFiat); + + let customTipAmount; + + const tipAmounts = ['None', '10%', '15%', '20%']; + + const handleTipButtonClick = (amount) => { + customTipAmount = ''; + if (amount === 'None') { + tipPercent = 0; + } else { + tipPercent = amount.slice(0, 2); + } + }; + + const handleCustomTipAmount = (e) => { + tipPercent = 0; + customTipAmount = e.target.value; + }; + + onMount(() => { + const qrCode = new QRCodeStyling({ + width: window.screen.width < 640 ? 250 : 300, + type: 'svg', + data: text, + image: '/images/invoice.svg', + dotsOptions: { + type: 'rounded' + }, + imageOptions: { + hideBackgroundDots: false + } + }); + + qrCode.append(document.getElementById('qr')); + }); </script> -{#if $user} - {text} - {$invoiceAmount} - {$rate} - {$user.username} - {#each $user.payments as p} - {p.amount}<br /> - {/each} -{/if} +<button class="ml-5 md:ml-20 mt-5 md:mt-10" on:click={() => goto('/receive')}> + <Icon icon="arrow-left" style="w-10" /> +</button> + +<div class="flex justify-center items-center mb-20 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" + > + {#if $invoiceAmount && $invoiceAmountFiat && $rate && text} + <!-- tipping section --> + <div class="text-center space-y-5"> + <h1 class="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 = ''; + tipPercent = e.target.value; + }} + /> + + <div> + {#each tipAmounts as amount} + <button + class="bg-primary w-20 py-3 m-1 rounded-2xl font-semibold {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 + 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 {!customTipAmount + ? 'opacity-50' + : 'opacity-100'}" + disabled={!customTipAmount}>Done</button + > + </div> + </div> + </div> + + <div + class="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"> + <span class="text-secondary" + >Scan to pay <span class="text-black font-semibold">insert name here</span></span + > + + <div + id="qr" + class="border border-lightgrey rounded-3xl lg:p-5 flex justify-center items-center" + /> + + <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">{`(${invoiceAmountFormatted} 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> + {:else} + <div + class="w-[94vw] md:w-[375px] h-[381px] md:h-[269px] animate-pulse bg-gray-400 rounded-xl" + /> + <div + class="h-[1px] lg:h-[600px] w-full lg:w-[1px] bg-lightgrey rounded-xl" + id="section-divider" + /> + <div + class="w-full md:w-[342px] h-[454px] md:h-[494px] animate-pulse bg-gray-400 rounded-xl" + /> + {/if} + </div> +</div> + +<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; + } +</style> diff --git a/src/routes/receive.svelte b/src/routes/receive.svelte index 338bc06c..095a2ba3 100644 --- a/src/routes/receive.svelte +++ b/src/routes/receive.svelte @@ -2,7 +2,7 @@ // 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, rate } from '$lib/store'; + import { invoiceAmount, invoiceAmountFiat, rate } from '$lib/store'; let useFiat = true; let amountFiat = 0; @@ -10,6 +10,9 @@ let message = ''; $: $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 ); @@ -63,6 +66,7 @@ amountSats = amountSats.slice(0, amountSats.length - 1); if (amountSats.length === 0) { amountSats = 0; + amountFiat = 0; } } } else if (value !== '<' && parseInt(amountSats + value) > 100000000) { @@ -77,8 +81,8 @@ <AppHeader /> <!-- step 1 input amount and generate QR code --> -{#if $rate} - <div class="flex justify-center items-center mt-20 mb-3 px-3"> +<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"> @@ -165,11 +169,7 @@ > </div> </div> - </div> -{:else} - <div class="px-3"> - <div - class="w-full md:w-[300px] h-[475px] md:h-[485px] animate-pulse bg-gray-400 mt-20 mb-3 mx-auto rounded-xl" - /> - </div> -{/if} + {:else} + <div class="w-full md:w-[300px] h-[475px] md:h-[485px] animate-pulse bg-gray-400 rounded-xl" /> + {/if} +</div> diff --git a/static/icons/arrow-left.svg b/static/icons/arrow-left.svg new file mode 100644 index 00000000..64ded577 --- /dev/null +++ b/static/icons/arrow-left.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 19L3 12M3 12L10 5M3 12L21 12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/static/images/invoice.svg b/static/images/invoice.svg new file mode 100644 index 00000000..4c9cbce9 --- /dev/null +++ b/static/images/invoice.svg @@ -0,0 +1,4 @@ +<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="72" height="72" rx="24" fill="#FA9514"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M39.1208 14.5116C40.1198 14.8265 40.7992 15.753 40.7992 16.8006V28.8006L50.3992 28.8006C51.2941 28.8006 52.1147 29.2985 52.528 30.0922C52.9412 30.886 52.8786 31.8437 52.3654 32.5769L35.5654 56.5769C34.9647 57.435 33.8768 57.8045 32.8777 57.4895C31.8786 57.1746 31.1992 56.2481 31.1992 55.2006L31.1992 43.2006H21.5992C20.7043 43.2006 19.8838 42.7027 19.4705 41.9089C19.0572 41.1152 19.1199 40.1574 19.6331 39.4243L36.4331 15.4243C37.0338 14.5661 38.1217 14.1967 39.1208 14.5116Z" fill="white"/> +</svg> diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 86d5d1cf..4be763a3 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -6,7 +6,8 @@ module.exports = { colors: { primary: '#F5F7FA', secondary: '#858A92', - gradient: '#C3CFE2' + gradient: '#C3CFE2', + lightgrey: '#eaecef' } } }, |
