diff options
| -rw-r--r-- | src/components/Payments.svelte | 11 | ||||
| -rw-r--r-- | src/locales/en.json | 6 | ||||
| -rw-r--r-- | src/routes/(app)/settings/+layout.svelte | 25 | ||||
| -rw-r--r-- | src/routes/(app)/settings/account/+page.server.ts | 6 | ||||
| -rw-r--r-- | src/routes/(app)/settings/account/+page.svelte | 377 | ||||
| -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 | 169 |
9 files changed, 451 insertions, 450 deletions
diff --git a/src/components/Payments.svelte b/src/components/Payments.svelte index 814a5b4e..36633089 100644 --- a/src/components/Payments.svelte +++ b/src/components/Payments.svelte @@ -6,6 +6,7 @@ import { t } from "$lib/translations"; import locales from "$lib/locales"; const { fund, locale, user, payments } = $props(); + const language = $derived(user?.language || "en"); </script> <div class="text-base"> @@ -106,7 +107,11 @@ {#if p.type === types.ecash} {amount > 0 ? $t("payments.redeemed") : $t("payments.minted")} {:else} - {amount > 0 ? (p.confirmed ? $t("payments.received") : $t("payments.pending")) : $t("payments.sent")} + {amount > 0 + ? p.confirmed + ? $t("payments.received") + : $t("payments.pending") + : $t("payments.sent")} {/if} </div> </div> @@ -116,12 +121,12 @@ <div class="text-secondary text-right text-sm my-auto col-span-3"> <div> {format(new Date(p.created), "h:mm aaa", { - locale: locales[user.language], + locale: locales[language], })} </div> <div> {format(new Date(p.created), "MMM d, yy", { - locale: locales[user.language], + locale: locales[language], })} </div> </div> diff --git a/src/locales/en.json b/src/locales/en.json index ca580e41..d6d9ca89 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -294,6 +294,7 @@ "placeholder": "Enter an address, invoice or username" }, "settings": { + "nostr": "Nostr", "name": "Name", "integrations": "Integrations", "syncWithExtension": "Sync with extension", @@ -325,7 +326,7 @@ "nwc": "Nostr wallet connect (NWC)", "nwcDescription": "Enable nostr apps to send payments from your account", "seedDescription": "This 12 word seed phrase is used to derive your nostr private key and your self-custodial wallet keys which determine your auto-withdrawal addresses. Back it up and don't let anyone see it.", - "emailDescription": "For password resets and payment notifications only", + "emailDescription": "For password resets and payment notifications", "hideNsec": "Hide nostr secret key", "saved": "Settings saved!", "verifying": "Verification email sent", @@ -340,7 +341,8 @@ "accountId": "Account Secret", "header": "Settings", "ACCOUNT": "PROFILE", - "POINT_OF_SALE": "SETTINGS", + "POINT_OF_SALE": "ACCOUNT", + "NOSTR": "NOSTR", "SECURITY": "SECURITY", "locale": "Language", "username": "Username", diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte index 2fe95813..33398ed5 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> diff --git a/src/routes/(app)/settings/account/+page.server.ts b/src/routes/(app)/settings/account/+page.server.ts index 4145935e..f0cc700e 100644 --- a/src/routes/(app)/settings/account/+page.server.ts +++ b/src/routes/(app)/settings/account/+page.server.ts @@ -1,5 +1,11 @@ +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/account/+page.svelte b/src/routes/(app)/settings/account/+page.svelte index d4178fd1..c5c75d88 100644 --- a/src/routes/(app)/settings/account/+page.svelte +++ b/src/routes/(app)/settings/account/+page.svelte @@ -1,58 +1,98 @@ <script> - import { tick } from "svelte"; - import { avatar, banner as bannerStore } from "$lib/store"; - import { t } from "$lib/translations"; + 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 } = $derived(data); - let { id } = user; - let { about, banner, picture, display, email, username, verified } = - $state(user); + let { user } = $state(data); + let { rates, subscriptions } = data; + let { currency, email, verified } = $state(user); + let rate = rates[currency]; - let avatarFile, - avatarInput = $state(), - bannerFile, - bannerInput = $state(); + let fiats = Object.keys(rates).sort((a, b) => a.localeCompare(b)); + let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); - let selectAvatar = () => avatarInput.click(); - let selectBanner = () => bannerInput.click(); + let editingReserve = $state(), + editingThreshold = $state(), + doneReserve = $state(), + doneThreshold; + let doneEditing = () => { + editingReserve = false; + editingThreshold = false; + }; - let percent; - let progress = async (event) => { - percent = Math.round((event.loaded / event.total) * 100); + let editReserve = async () => { + editingReserve = true; + await tick(); + reserveEl.focus(); }; - let tooLarge = $state({}); + 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; - let handleFile = async ({ target }, type) => { - tooLarge[type] = false; - let file = target.files[0]; - if (!file) return; + onMount(async () => { + if (!browser) return; - if (file.size > 10000000) return (tooLarge[type] = true); + pm = + navigator?.serviceWorker && + (await navigator.serviceWorker.getRegistration()).pushManager; - if (type === "picture") { - $avatar = { id, file, type, progress }; - } else if (type === "banner") { - $bannerStore = { id, file, type, progress }; + 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; } + }); - 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; @@ -60,19 +100,21 @@ </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 +128,181 @@ > <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> - {: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> +<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 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> + <p class="text-secondary mt-1 w-9/12"> + {#if permission === "denied"} + {$t("user.settings.pushNotificationsDisabled")} + {:else} + {$t("user.settings.pushNotificationsDesc")} + {/if} + </p> +</div> - {#if tooLarge["avatar"]} - <div class="text-red-600">Max file size 10MB</div> - {/if} +<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-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} +{#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> - <button - type="button" - class="btn !w-auto" - onclick={selectBanner} - onkeydown={selectBanner}>{$t("user.settings.select")}</button + <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" > - <input - type="file" - class="hidden" - bind:this={bannerInput} - onchange={(e) => handleFile(e, "banner")} - /> + <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} + /> - {#if tooLarge["banner"]} - <div class="text-red-600">Max file size 10MB</div> - {/if} -</div> + <button + bind:this={doneReserve} + type="button" + onclick={doneEditing} + class="btn">Ok</button + > + </div> + </div> +{/if} -<div> - <label for="about" class="font-bold mb-1 block" - >{$t("user.settings.about")}</label +{#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" > - <textarea - type="text" - name="about" - bind:value={about} - placeholder={$t("user.settings.aboutPlaceholder")} - ></textarea> -</div> + <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/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..bee50d39 --- /dev/null +++ b/src/routes/(app)/settings/profile/+page.svelte @@ -0,0 +1,169 @@ +<script> + import { tick } from "svelte"; + import { avatar, banner as bannerStore } from "$lib/store"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + + let { data } = $props(); + let { user } = $derived(data); + let { id } = user; + let { about, banner, picture, display, username } = + $state(user); + + let avatarFile, + avatarInput = $state(), + bannerFile, + 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> + +<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> |
