diff options
| author | Jurjen de Vries <[email protected]> | 2025-09-11 00:48:15 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-09-11 00:48:15 +0200 |
| commit | 0c69c9848c6b98c92609d209103efc60003c9723 (patch) | |
| tree | 445121def885f13d0f80c06864ad2ff4d5ef95a1 /src/routes/(app)/settings | |
| parent | 2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff) | |
| parent | 9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff) | |
| download | coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip | |
Merge branch 'coinos:master' into master
Diffstat (limited to 'src/routes/(app)/settings')
| -rw-r--r-- | src/routes/(app)/settings/+layout.svelte | 30 | ||||
| -rw-r--r-- | src/routes/(app)/settings/account/+page.server.ts | 8 | ||||
| -rw-r--r-- | src/routes/(app)/settings/account/+page.svelte | 407 | ||||
| -rw-r--r-- | src/routes/(app)/settings/nostr/+page.svelte | 39 | ||||
| -rw-r--r-- | src/routes/(app)/settings/preferences/+page.server.ts | 11 | ||||
| -rw-r--r-- | src/routes/(app)/settings/preferences/+page.svelte | 291 | ||||
| -rw-r--r-- | src/routes/(app)/settings/profile/+page.server.ts | 5 | ||||
| -rw-r--r-- | src/routes/(app)/settings/profile/+page.svelte | 181 | ||||
| -rw-r--r-- | src/routes/(app)/settings/security/+page.svelte | 31 | ||||
| -rw-r--r-- | src/routes/(app)/settings/square/+page.server.ts | 18 | ||||
| -rw-r--r-- | src/routes/(app)/settings/square/+page.svelte | 0 |
11 files changed, 541 insertions, 480 deletions
diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte index 2fe95813..3c2ebd88 100644 --- a/src/routes/(app)/settings/+layout.svelte +++ b/src/routes/(app)/settings/+layout.svelte @@ -36,8 +36,9 @@ }; let tabs = [ - { name: "account", key: "ACCOUNT" }, - { name: "preferences", key: "POINT_OF_SALE" }, + { name: "profile", key: "ACCOUNT" }, + { name: "account", key: "POINT_OF_SALE" }, + { name: "nostr", key: "NOSTR" }, { name: "security", key: "SECURITY" }, ]; @@ -208,16 +209,16 @@ {$t("user.settings.header")} </h1> - <div class="flex justify-around items-center border-b pb-3"> - {#each tabs.filter((t) => t.name !== "shopify") as { name, key }} - <a href={`/settings/${name}`}> - <button - type="button" - class="hover:opacity-80" - class:font-bold={tab === name || - (name === "preferences" && ["nostr", "shopify"].includes(tab))} - >{$t(`user.settings.${key}`)}</button - > + <div + class="flex flex-wrap justify-around items-center border-b pb-3" + > + {#each tabs as { name, key }} + <a + href={`/settings/${name}`} + class="px-4 py-2 w-1/2 sm:w-1/4 flex-shrink-0 hover:opacity-80 transition-opacity duration-150 text-center" + class:font-bold={name === tab} + > + {$t(`user.settings.${key}`)} </a> {/each} </div> @@ -227,17 +228,18 @@ </div> <div - class="z-10 fixed bottom-0 bg-base-100 shadow border-t border-t-8 border-accent w-full flex justify-center items-center py-3" + class="z-10 fixed bottom-0 bg-base-100 shadow border-accent w-full flex justify-center items-center py-3" > <button bind:this={$save} type="submit" - class="btn !w-auto" + class="btn btn-accent !w-auto" class:bg-base-200={submitting} > {#if submitting} <Spinner /> {:else} + <iconify-icon icon="ph:floppy-disk-bold" width={32}></iconify-icon> <div class="my-auto">{$t("user.settings.saveSettings")}</div> {/if} </button> diff --git a/src/routes/(app)/settings/account/+page.server.ts b/src/routes/(app)/settings/account/+page.server.ts index 4145935e..ebb90f05 100644 --- a/src/routes/(app)/settings/account/+page.server.ts +++ b/src/routes/(app)/settings/account/+page.server.ts @@ -1,4 +1,12 @@ +import getRates from "$lib/rates"; import updateUser from "$lib/settings"; +import { auth, get } from "$lib/utils"; + +export async function load({ cookies }) { + const rates = await getRates(); + const connect = await get("/square/connect", auth(cookies)); + return { connect, rates }; +} export const actions = { default: updateUser, diff --git a/src/routes/(app)/settings/account/+page.svelte b/src/routes/(app)/settings/account/+page.svelte index d4178fd1..8b38ffe8 100644 --- a/src/routes/(app)/settings/account/+page.svelte +++ b/src/routes/(app)/settings/account/+page.svelte @@ -1,78 +1,123 @@ <script> - import { tick } from "svelte"; - import { avatar, banner as bannerStore } from "$lib/store"; - import { t } from "$lib/translations"; + import { run } from "svelte/legacy"; + import { theme } from "$lib/store"; + + import { onMount, tick } from "svelte"; + import { browser } from "$app/environment"; + import Numpad from "$comp/Numpad.svelte"; + import LocaleSelector from "$comp/LocaleSelector.svelte"; + import Toggle from "$comp/Toggle.svelte"; + import { locale, t } from "$lib/translations"; + import { post, success, fail } from "$lib/utils"; import { page } from "$app/stores"; + import { PUBLIC_VAPID_PUBKEY } from "$env/static/public"; let { data } = $props(); - let { user } = $derived(data); - let { id } = user; - let { about, banner, picture, display, email, username, verified } = - $state(user); + let { user } = $state(data); + let { connect, rates, subscriptions } = data; + let { currency, email, tip, verified } = $state(user); + let rate = rates[currency]; + + let fiats = Object.keys(rates).sort((a, b) => a.localeCompare(b)); + let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); - let avatarFile, - avatarInput = $state(), - bannerFile, - bannerInput = $state(); + let editingReserve = $state(), + editingThreshold = $state(), + doneReserve = $state(), + doneThreshold; + let doneEditing = () => { + editingReserve = false; + editingThreshold = false; + }; - let selectAvatar = () => avatarInput.click(); - let selectBanner = () => bannerInput.click(); + let editReserve = async () => { + editingReserve = true; + await tick(); + reserveEl.focus(); + }; - let percent; - let progress = async (event) => { - percent = Math.round((event.loaded / event.total) * 100); + let editThreshold = async () => { + editingThreshold = true; + await tick(); + thresholdEl.focus(); }; - let tooLarge = $state({}); + if (!user.threshold) user.threshold = 1000000; + if (!user.reserve) user.reserve = 100000; + let reserveEl = $state(), + thresholdEl = $state(); + + let push = $state(), + pm, + subscription; + + onMount(async () => { + if (!browser) return; - let handleFile = async ({ target }, type) => { - tooLarge[type] = false; - let file = target.files[0]; - if (!file) return; + pm = + navigator?.serviceWorker && + (await navigator.serviceWorker.getRegistration()).pushManager; - if (file.size > 10000000) return (tooLarge[type] = true); + permission = await pm.permissionState({ + userVisibleOnly: true, + applicationServerKey: PUBLIC_VAPID_PUBKEY, + }); - if (type === "picture") { - $avatar = { id, file, type, progress }; - } else if (type === "banner") { - $bannerStore = { id, file, type, progress }; + if (permission === "granted") { + subscription = await pm.getSubscription(); + if (subscriptions.includes(JSON.stringify(subscription))) push = true; } + }); - var reader = new FileReader(); - reader.onload = async (e) => { - if (type === "picture") { - $avatar.src = e.target.result; - } else if (type === "banner") { - $bannerStore.src = e.target.result; - } - }; + let permission = $state(); + let updateNotifications = async (push) => { + if (!browser || !pm) return (push = false); - reader.readAsDataURL(file); - }; + if (subscription && !push) { + return post("/subscription/delete", { subscription }); + } - let url = $derived(`${$page.url.host}/${username}`); - let full = $derived(`${$page.url.protocol}//${url}`); - let addr = $derived(`${username}@${$page.url.host}`); + if (push && permission !== "denied") { + subscription = await pm.subscribe({ + userVisibleOnly: true, + applicationServerKey: PUBLIC_VAPID_PUBKEY, + }); + } + + if (subscription) { + await post("/subscription", { subscription }); + } + }; + run(() => { + user.language = $locale; + }); + run(() => { + updateNotifications(push); + }); $effect(() => { if (email !== user.email) verified = false; }); + + let revoke = () => {}; </script> <div> - <label for="username" class="font-bold mb-1 block" - >{$t("user.settings.username")}</label + <label for="language" class="font-bold block mb-1" + >{$t("user.settings.locale")}</label > - <div class="flex mb-2"> - <input type="text" name="username" bind:value={username} /> - </div> + <LocaleSelector style="select-styles block py-3 w-full" /> </div> <div> - <label for="display" class="font-bold mb-1 block" - >{$t("user.settings.displayName")}</label + <label for="currency" class="font-bold block mb-1" + >{$t("user.settings.localCurrency")}</label > - <input type="text" name="display" bind:value={display} /> + <select name="currency" value={currency}> + {#each fiats as fiat} + <option value={fiat}>{fiat}</option> + {/each} + </select> </div> <div class="space-y-1 relative"> @@ -86,108 +131,210 @@ > <input type="text" name="email" class="clean" bind:value={email} /> {#if verified} - <iconify-icon noobserver icon="ph:check-bold" class="text-success ml-auto" width="32" + <iconify-icon + noobserver + icon="ph:check-bold" + class="text-success ml-auto" + width="32" ></iconify-icon> {:else if email} - <iconify-icon noobserver icon="ph:clock-bold" class="text-warning ml-auto" width="32" + <iconify-icon + noobserver + icon="ph:clock-bold" + class="text-warning ml-auto" + width="32" ></iconify-icon> {/if} </label> </div> -<div class="space-y-2"> - <span class="font-bold">{$t("user.settings.profileImage")}</span> +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.notifications")}</span> + <Toggle id="notify" bind:value={user.notify} /> + </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.notificationsDesc")} + </p> +</div> - <div class="flex"> - {#if $avatar || picture} - <div - class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer" - onclick={selectAvatar} - onkeydown={selectAvatar} - > - <img - src={$avatar?.src || picture} - class="absolute w-full h-full object-cover object-center visible overflow-hidden" - alt={username} - /> - </div> +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.pushNotifications")}</span> + {#if permission !== "denied"} + <Toggle id="push" bind:value={push} /> + {/if} + </div> + <p class="text-secondary mt-1 w-9/12"> + {#if permission === "denied"} + {$t("user.settings.pushNotificationsDisabled")} {:else} - <div - class="rounded-full border-4 border-base-100 p-4 bg-base-200 w-24 h-24 my-auto hover:opacity-80 cursor-pointer" - onclick={selectAvatar} - onkeydown={selectAvatar} - ></div> + {$t("user.settings.pushNotificationsDesc")} {/if} - <div class="ml-2 p-2"> - <!-- found missing translation --> - <button - type="button" - class="btn" - onclick={selectAvatar} - onkeydown={selectAvatar}>{$t("user.settings.select")}</button - > - <input - type="file" - class="hidden" - bind:this={avatarInput} - onchange={(e) => handleFile(e, "picture")} - /> - </div> + </p> +</div> + +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.tipPrompt")}</span> + <Toggle id="prompt" bind:value={user.prompt} /> </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.tipPromptDescription")} + </p> +</div> + +<div class="space-y-1 relative"> + <label for="tip" class="font-bold block">{$t("user.settings.tip")}</label> + <p class="text-secondary"> + {$t("user.settings.tipDesc")} + </p> - {#if tooLarge["avatar"]} - <div class="text-red-600">Max file size 10MB</div> - {/if} + <label + class="input input-bordered border-primary input-lg rounded-2xl flex items-center gap-2" + > + <input type="text" name="tip" class="clean" bind:value={tip} /> + </label> </div> -<div class="space-y-2"> +<div> <div class="flex justify-between items-center"> - <span class="font-bold">{$t("user.settings.bannerImage")}</span> + <span class="font-bold">{$t("user.settings.autoWithdraw")}</span> + <Toggle id="autowithdraw" bind:value={user.autowithdraw} /> </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.autoWithdrawDescription")} + </p> +</div> - {#if $bannerStore || banner} - <img - src={$bannerStore ? $bannerStore.src : banner} - class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80" - onclick={selectBanner} - onkeydown={selectBanner} - alt="Banner" - /> - {:else} - <div - class="bg-base-200 w-full h-48 mb-4 cursor-pointer hover:opacity-80" - onclick={selectBanner} - onkeydown={selectBanner} - alt="Banner" - ></div> - {/if} +<div class:hidden={!user.autowithdraw}> + <div class="mb-2"> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.destination")}</label + > + <textarea + name="destination" + placeholder={$t("user.settings.destinationPlaceholder")} + onkeypress={keypress} + class="w-full p-4 border rounded-xl h-48" + bind:value={user.destination} + ></textarea> + </div> - <button - type="button" - class="btn !w-auto" - onclick={selectBanner} - onkeydown={selectBanner}>{$t("user.settings.select")}</button - > - <input - type="file" - class="hidden" - bind:this={bannerInput} - onchange={(e) => handleFile(e, "banner")} - /> + <div> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.threshold")}</label + > + <button type="button" class="flex w-full" onclick={editThreshold}> + <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200"> + <iconify-icon + noobserver + icon="ph:lightning-fill" + class="text-yellow-300" + ></iconify-icon> + </div> + <div + class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left" + > + {user.threshold} + </div> + <input type="hidden" name="threshold" bind:value={user.threshold} /> + </button> + <p class="text-secondary mt-1"> + {$t("user.settings.thresholdDesc")} + </p> + </div> - {#if tooLarge["banner"]} - <div class="text-red-600">Max file size 10MB</div> - {/if} + <div> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.reserve")}</label + > + <button type="button" class="flex w-full" onclick={editReserve}> + <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200"> + <iconify-icon + noobserver + icon="ph:lightning-fill" + class="text-yellow-300" + ></iconify-icon> + </div> + <div + class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left" + > + {user.reserve} + </div> + <input type="hidden" name="reserve" bind:value={user.reserve} /> + </button> + <p class="text-secondary mt-1"> + {$t("user.settings.reserveDesc")} + </p> + </div> </div> -<div> - <label for="about" class="font-bold mb-1 block" - >{$t("user.settings.about")}</label +{#if editingThreshold} + <div + class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 max-w-lg mx-auto" > - <textarea - type="text" - name="about" - bind:value={about} - placeholder={$t("user.settings.aboutPlaceholder")} - ></textarea> -</div> + <div class="relative p-5 border shadow-lg rounded-md bg-base-100 space-y-5"> + <h1 class="text-center text-2xl font-semibold"> + {$t("user.settings.threshold")} + </h1> + <Numpad + bind:amount={user.threshold} + {currency} + bind:rate + bind:submit={doneReserve} + bind:element={thresholdEl} + /> + + <button + bind:this={doneReserve} + type="button" + onclick={doneEditing} + class="btn">Ok</button + > + </div> + </div> +{/if} + +{#if editingReserve} + <div + class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 mx-auto max-w-lg" + > + <div + class="relative mx-auto p-5 border shadow-lg rounded-md bg-base-100 space-y-5 text-center" + > + <h1 class="text-2xl font-semibold"> + {$t("user.settings.reserve")} + </h1> + <Numpad + bind:amount={user.reserve} + bind:currency + bind:rate + bind:submit={doneReserve} + bind:element={reserveEl} + /> + <button + bind:this={doneReserve} + type="button" + onclick={doneEditing} + class="btn">Ok</button + > + </div> + </div> +{/if} + +{#if connect !== "connected"} + <a href={connect} class="btn flex"> + <img src="/images/square.svg" class="w-12" /> + <div>{$t("user.settings.connectSquare")}</div></a + > +{:else} + <button type="button" onclick={revoke} class="btn"> + <img + src="/images/square.svg" + class="w-12" + class:invert={$theme === "dark"} + /> + <div>{$t("user.settings.revokeSquare")}</div> + </button> +{/if} diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 421b590a..7168f4f7 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -163,25 +163,26 @@ <div class="space-y-2"> <div class="font-bold">{$t("user.nostrPubkey")}</div> - <div class="flex gap-4"> - <textarea name="pubkey" bind:value={npub} rows={3}> </textarea> - <div class="flex my-auto gap-1"> - <button - type="button" - class="my-auto btn btn-circle" - onclick={() => copy(npub)} - aria-label="Copy" - ><iconify-icon noobserver icon="ph:copy-bold" width="32" - ></iconify-icon></button - > - <a - href={`/qr/${encodeURIComponent(npub)}`} - class="my-auto btn btn-circle" - aria-label="QR" - > - <iconify-icon noobserver icon="ph:qr-code-bold" width="32" - ></iconify-icon> - </a> + <textarea name="pubkey" bind:value={npub} rows={3}></textarea> + <div> + <div class="flex my-auto gap-1"> + <button + type="button" + class="my-auto btn btn-circle !w-auto grow" + onclick={() => copy(npub)} + aria-label="Copy" + ><iconify-icon noobserver icon="ph:copy-bold" width="32" + ></iconify-icon> {$t("accounts.copy")}</button + > + + <a + href={`/qr/${encodeURIComponent(npub)}`} + class="my-auto btn btn-circle !w-auto grow" + aria-label="QR" + > + <iconify-icon noobserver icon="ph:qr-code-bold" width="32"></iconify-icon> +{$t("accounts.qr")} + </a> </div> </div> diff --git a/src/routes/(app)/settings/preferences/+page.server.ts b/src/routes/(app)/settings/preferences/+page.server.ts deleted file mode 100644 index f0cc700e..00000000 --- a/src/routes/(app)/settings/preferences/+page.server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import getRates from "$lib/rates"; -import updateUser from "$lib/settings"; - -export async function load() { - const rates = await getRates(); - return { rates }; -} - -export const actions = { - default: updateUser, -}; diff --git a/src/routes/(app)/settings/preferences/+page.svelte b/src/routes/(app)/settings/preferences/+page.svelte deleted file mode 100644 index 0f30c61b..00000000 --- a/src/routes/(app)/settings/preferences/+page.svelte +++ /dev/null @@ -1,291 +0,0 @@ -<script> - import { run } from "svelte/legacy"; - - import { onMount, tick } from "svelte"; - import { browser } from "$app/environment"; - import Numpad from "$comp/Numpad.svelte"; - import LocaleSelector from "$comp/LocaleSelector.svelte"; - import Toggle from "$comp/Toggle.svelte"; - import { locale, t } from "$lib/translations"; - import { post, success, fail } from "$lib/utils"; - import { page } from "$app/stores"; - import { PUBLIC_VAPID_PUBKEY } from "$env/static/public"; - - let { data } = $props(); - let { user } = $state(data); - let { rates, subscriptions } = data; - let { currency } = user; - let rate = rates[currency]; - - let fiats = Object.keys(rates).sort((a, b) => a.localeCompare(b)); - let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); - - let editingReserve = $state(), - editingThreshold = $state(), - doneReserve = $state(), - doneThreshold; - let doneEditing = () => { - editingReserve = false; - editingThreshold = false; - }; - - let editReserve = async () => { - editingReserve = true; - await tick(); - reserveEl.focus(); - }; - - let editThreshold = async () => { - editingThreshold = true; - await tick(); - thresholdEl.focus(); - }; - - if (!user.threshold) user.threshold = 1000000; - if (!user.reserve) user.reserve = 100000; - let reserveEl = $state(), - thresholdEl = $state(); - - let push = $state(), - pm, - subscription; - - onMount(async () => { - if (!browser) return; - - pm = - navigator?.serviceWorker && - (await navigator.serviceWorker.getRegistration()).pushManager; - - permission = await pm.permissionState({ - userVisibleOnly: true, - applicationServerKey: PUBLIC_VAPID_PUBKEY, - }); - - if (permission === "granted") { - subscription = await pm.getSubscription(); - if (subscriptions.includes(JSON.stringify(subscription))) push = true; - } - }); - - let permission = $state(); - let updateNotifications = async (push) => { - if (!browser || !pm) return (push = false); - - if (subscription && !push) { - return post("/subscription/delete", { subscription }); - } - - if (push && permission !== "denied") { - subscription = await pm.subscribe({ - userVisibleOnly: true, - applicationServerKey: PUBLIC_VAPID_PUBKEY, - }); - } - - if (subscription) { - await post("/subscription", { subscription }); - } - }; - run(() => { - user.language = $locale; - }); - run(() => { - updateNotifications(push); - }); -</script> - -<div> - <label for="language" class="font-bold block mb-1" - >{$t("user.settings.locale")}</label - > - <LocaleSelector style="select-styles block py-3 w-full" /> -</div> - -<div> - <label for="currency" class="font-bold block mb-1" - >{$t("user.settings.localCurrency")}</label - > - <select name="currency" value={currency}> - {#each fiats as fiat} - <option value={fiat}>{fiat}</option> - {/each} - </select> -</div> - -<div> - <label for="currency" class="font-bold block mb-1" - >{$t("user.settings.integrations")}</label - > - <div class="flex flex-wrap gap-2 justify-center"> - <a href="/settings/nostr" class="block grow"> - <button type="button" class="btn"> - <img src="/images/nostr.png" class="w-8" /> - <div class="my-auto">{$t("user.settings.nostr")}</div> - </button> - </a> - - <a href="/settings/shopify" class="block grow"> - <button type="button" class="btn"> - <img src="/icons/shopify.svg" class="w-8" /> - <div class="my-auto">{$t("user.settings.shopify")}</div> - </button> - </a> - </div> -</div> - -<div> - <div class="flex justify-between items-center"> - <span class="font-bold">{$t("user.settings.notifications")}</span> - <Toggle id="notify" bind:value={user.notify} /> - </div> - <p class="text-secondary mt-1 w-9/12"> - {$t("user.settings.notificationsDesc")} - </p> -</div> - -<div> - <div class="flex justify-between items-center"> - <span class="font-bold">{$t("user.settings.pushNotifications")}</span> - {#if permission !== "denied"} - <Toggle id="push" bind:value={push} /> - {/if} - </div> - <p class="text-secondary mt-1 w-9/12"> - {#if permission === "denied"} - {$t("user.settings.pushNotificationsDisabled")} - {:else} - {$t("user.settings.pushNotificationsDesc")} - {/if} - </p> -</div> - -<div> - <div class="flex justify-between items-center"> - <span class="font-bold">{$t("user.settings.tipPrompt")}</span> - <Toggle id="prompt" bind:value={user.prompt} /> - </div> - <p class="text-secondary mt-1 w-9/12"> - {$t("user.settings.tipPromptDescription")} - </p> -</div> - -<div> - <div class="flex justify-between items-center"> - <span class="font-bold">{$t("user.settings.autoWithdraw")}</span> - <Toggle id="autowithdraw" bind:value={user.autowithdraw} /> - </div> - <p class="text-secondary mt-1 w-9/12"> - {$t("user.settings.autoWithdrawDescription")} - </p> -</div> - -{#if user.autowithdraw} - <div class="mb-2"> - <label for="display" class="font-bold mb-1 block" - >{$t("user.settings.destination")}</label - > - <textarea - name="destination" - placeholder={$t("user.settings.destinationPlaceholder")} - onkeypress={keypress} - class="w-full p-4 border rounded-xl h-48" - bind:value={user.destination} - ></textarea> - </div> - - <div> - <label for="display" class="font-bold mb-1 block" - >{$t("user.settings.threshold")}</label - > - <button type="button" class="flex w-full" onclick={editThreshold}> - <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200"> - <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300" - ></iconify-icon> - </div> - <div - class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left" - > - {user.threshold} - </div> - <input type="hidden" name="threshold" bind:value={user.threshold} /> - </button> - <p class="text-secondary mt-1"> - {$t("user.settings.thresholdDesc")} - </p> - </div> - - <div> - <label for="display" class="font-bold mb-1 block" - >{$t("user.settings.reserve")}</label - > - <button type="button" class="flex w-full" onclick={editReserve}> - <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200"> - <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300" - ></iconify-icon> - </div> - <div - class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left" - > - {user.reserve} - </div> - <input type="hidden" name="reserve" bind:value={user.reserve} /> - </button> - <p class="text-secondary mt-1"> - {$t("user.settings.reserveDesc")} - </p> - </div> -{/if} - -{#if editingThreshold} - <div - class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 max-w-lg mx-auto" - > - <div class="relative p-5 border shadow-lg rounded-md bg-base-100 space-y-5"> - <h1 class="text-center text-2xl font-semibold"> - {$t("user.settings.threshold")} - </h1> - <Numpad - bind:amount={user.threshold} - {currency} - bind:rate - bind:submit={doneReserve} - bind:element={thresholdEl} - /> - - <button - bind:this={doneReserve} - type="button" - onclick={doneEditing} - class="btn">Ok</button - > - </div> - </div> -{/if} - -{#if editingReserve} - <div - class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 mx-auto max-w-lg" - > - <div - class="relative mx-auto p-5 border shadow-lg rounded-md bg-base-100 space-y-5 text-center" - > - <h1 class="text-2xl font-semibold"> - {$t("user.settings.reserve")} - </h1> - <Numpad - bind:amount={user.reserve} - bind:currency - bind:rate - bind:submit={doneReserve} - bind:element={reserveEl} - /> - <button - bind:this={doneReserve} - type="button" - onclick={doneEditing} - class="btn">Ok</button - > - </div> - </div> -{/if} diff --git a/src/routes/(app)/settings/profile/+page.server.ts b/src/routes/(app)/settings/profile/+page.server.ts new file mode 100644 index 00000000..4145935e --- /dev/null +++ b/src/routes/(app)/settings/profile/+page.server.ts @@ -0,0 +1,5 @@ +import updateUser from "$lib/settings"; + +export const actions = { + default: updateUser, +}; diff --git a/src/routes/(app)/settings/profile/+page.svelte b/src/routes/(app)/settings/profile/+page.svelte new file mode 100644 index 00000000..3b7de9d3 --- /dev/null +++ b/src/routes/(app)/settings/profile/+page.svelte @@ -0,0 +1,181 @@ +<script> + import { tick } from "svelte"; + import { avatar, banner as bannerStore } from "$lib/store"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + import PasswordInput from "$comp/PasswordInput.svelte"; + + let { data } = $props(); + let { user } = $derived(data); + let { id } = user; + let { about, banner, picture, display, username } = $state(user); + + let avatarFile, + avatarInput = $state(), + bannerFile, + password = $state(), + revealPassword = $state(), + bannerInput = $state(); + + let selectAvatar = () => avatarInput.click(); + let selectBanner = () => bannerInput.click(); + + let percent; + let progress = async (event) => { + percent = Math.round((event.loaded / event.total) * 100); + }; + + let tooLarge = $state({}); + + let handleFile = async ({ target }, type) => { + tooLarge[type] = false; + let file = target.files[0]; + if (!file) return; + + if (file.size > 10000000) return (tooLarge[type] = true); + + if (type === "picture") { + $avatar = { id, file, type, progress }; + } else if (type === "banner") { + $bannerStore = { id, file, type, progress }; + } + + var reader = new FileReader(); + reader.onload = async (e) => { + if (type === "picture") { + $avatar.src = e.target.result; + } else if (type === "banner") { + $bannerStore.src = e.target.result; + } + }; + + reader.readAsDataURL(file); + }; + + let url = $derived(`${$page.url.host}/${username}`); + let full = $derived(`${$page.url.protocol}//${url}`); + let addr = $derived(`${username}@${$page.url.host}`); +</script> + +<div> + <label for="username" class="font-bold mb-1 block" + >{$t("user.settings.username")}</label + > + <div class="flex mb-2"> + <input type="text" name="username" bind:value={username} /> + </div> +</div> + +<input type="hidden" name="confirm" bind:value={password} /> + +<div> + <label for="password" class="block font-bold block mb-1" + >{$t("user.settings.newPassword")}</label + > + + <PasswordInput bind:value={password} placeholder="(Leave blank to keep unchanged)" /> +</div> + +<div> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.displayName")}</label + > + <input type="text" name="display" bind:value={display} /> +</div> + +<div class="space-y-2"> + <span class="font-bold">{$t("user.settings.profileImage")}</span> + + <div class="flex"> + {#if $avatar || picture} + <div + class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer" + onclick={selectAvatar} + onkeydown={selectAvatar} + > + <img + src={$avatar?.src || picture} + class="absolute w-full h-full object-cover object-center visible overflow-hidden" + alt={username} + /> + </div> + {:else} + <div + class="rounded-full border-4 border-base-100 p-4 bg-base-200 w-24 h-24 my-auto hover:opacity-80 cursor-pointer" + onclick={selectAvatar} + onkeydown={selectAvatar} + ></div> + {/if} + <div class="ml-2 p-2"> + <!-- found missing translation --> + <button + type="button" + class="btn" + onclick={selectAvatar} + onkeydown={selectAvatar}>{$t("user.settings.select")}</button + > + <input + type="file" + class="hidden!" + bind:this={avatarInput} + onchange={(e) => handleFile(e, "picture")} + /> + </div> + </div> + + {#if tooLarge["avatar"]} + <div class="text-red-600">Max file size 10MB</div> + {/if} +</div> + +<div class="space-y-2"> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.bannerImage")}</span> + </div> + + {#if $bannerStore || banner} + <img + src={$bannerStore ? $bannerStore.src : banner} + class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80" + onclick={selectBanner} + onkeydown={selectBanner} + alt="Banner" + /> + {:else} + <div + class="bg-base-200 w-full h-48 mb-4 cursor-pointer hover:opacity-80" + onclick={selectBanner} + onkeydown={selectBanner} + alt="Banner" + ></div> + {/if} + + <button + type="button" + class="btn !w-auto" + onclick={selectBanner} + onkeydown={selectBanner}>{$t("user.settings.select")}</button + > + <input + type="file" + class="hidden!" + bind:this={bannerInput} + onchange={(e) => handleFile(e, "banner")} + /> + + {#if tooLarge["banner"]} + <div class="text-red-600">Max file size 10MB</div> + {/if} +</div> + +<div> + <label for="about" class="font-bold mb-1 block" + >{$t("user.settings.about")}</label + > + <textarea + type="text" + name="about" + bind:value={about} + placeholder={$t("user.settings.aboutPlaceholder")} + ></textarea> +</div> diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte index 5d4b492f..8c38bca1 100644 --- a/src/routes/(app)/settings/security/+page.svelte +++ b/src/routes/(app)/settings/security/+page.svelte @@ -4,7 +4,6 @@ import { t } from "$lib/translations"; import Pin from "$comp/Pin.svelte"; import Qr from "$comp/Qr.svelte"; - import PasswordInput from "$comp/PasswordInput.svelte"; import { copy, post, success, fail } from "$lib/utils"; import { save, pin as current } from "$lib/store"; import { invalidate } from "$app/navigation"; @@ -21,9 +20,7 @@ newNsec = $state(), nsec = $state(), otp = $state(), - password = $state(), pin = $state(""), - revealPassword = $state(), revealSeed, revealNsec = $state(), token = $state(""), @@ -157,15 +154,6 @@ </script> <input type="hidden" name="newpin" value={disablingPin ? "delete" : pin} /> -<input type="hidden" name="confirm" bind:value={password} /> - -<div> - <label for="password" class="block font-bold block mb-1" - >{$t("user.settings.newPassword")}</label - > - - <PasswordInput bind:value={password} /> -</div> <div> <span class="font-bold mb-1" @@ -187,7 +175,8 @@ /> {:else} <button type="button" class="btn" onclick={togglePin}> - <iconify-icon noobserver + <iconify-icon + noobserver icon={haspin ? "ph:lock-key-open-bold" : "ph:lock-key-bold"} width="32" ></iconify-icon> @@ -220,12 +209,14 @@ </button> {:else if user.twofa} <button type="button" class="btn" onclick={startDisabling2fa}> - <iconify-icon noobserver icon="ph:device-mobile-bold" width="32"></iconify-icon> + <iconify-icon noobserver icon="ph:device-mobile-bold" width="32" + ></iconify-icon> <div class="my-auto">{$t("user.settings.twofaDisable")}</div> </button> {:else} <button type="button" class="btn" onclick={startEnabling2fa}> - <iconify-icon noobserver icon="ph:device-mobile-bold" width="32"></iconify-icon> + <iconify-icon noobserver icon="ph:device-mobile-bold" width="32" + ></iconify-icon> <div class="my-auto">{$t("user.settings.twofaSetup")}</div> </button> {/if} @@ -240,3 +231,13 @@ /> {/if} </div> + +<div> + <a href={`/account/savings`} class="contents"> + <button class="btn btn-lg w-full rounded-2xl whitespace-nowrap"> + <iconify-icon noobserver icon="ph:plus-circle-bold" width="32" + ></iconify-icon> + {$t("accounts.addAccount")} + </button> + </a> +</div> diff --git a/src/routes/(app)/settings/square/+page.server.ts b/src/routes/(app)/settings/square/+page.server.ts new file mode 100644 index 00000000..e00bdf1b --- /dev/null +++ b/src/routes/(app)/settings/square/+page.server.ts @@ -0,0 +1,18 @@ +import { auth, get } from "$lib/utils"; +import { fail, redirect } from "@sveltejs/kit"; + +export async function load({ cookies, url }) { + let connected; + + const query = url.searchParams.toString(); + + try { + await get(`/square/auth?${query}`, auth(cookies)); + connected = true; + } catch (e) { + const { message } = e as Error; + fail(400, { message }); + } + + if (connected) redirect(307, "/settings/account"); +} diff --git a/src/routes/(app)/settings/square/+page.svelte b/src/routes/(app)/settings/square/+page.svelte new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/routes/(app)/settings/square/+page.svelte |
