aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-05-03 08:43:35 -0700
committerAdam Soltys <[email protected]>2025-05-03 08:43:35 -0700
commitfbf90d1c6b199cea58613f0abe16614fa565a9e8 (patch)
tree54822d1f2cbc4c3672ab67b2eb2585e6af88e6cb
parent7849afdb52cf2669958c8055ccdab4b39e27a186 (diff)
downloadcoinos-ui-fbf90d1c6b199cea58613f0abe16614fa565a9e8.tar.gz
coinos-ui-fbf90d1c6b199cea58613f0abe16614fa565a9e8.zip
settings re-org
-rw-r--r--src/components/Payments.svelte11
-rw-r--r--src/locales/en.json6
-rw-r--r--src/routes/(app)/settings/+layout.svelte25
-rw-r--r--src/routes/(app)/settings/account/+page.server.ts6
-rw-r--r--src/routes/(app)/settings/account/+page.svelte377
-rw-r--r--src/routes/(app)/settings/preferences/+page.server.ts11
-rw-r--r--src/routes/(app)/settings/preferences/+page.svelte291
-rw-r--r--src/routes/(app)/settings/profile/+page.server.ts5
-rw-r--r--src/routes/(app)/settings/profile/+page.svelte169
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>