aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/settings
diff options
context:
space:
mode:
authorJurjen de Vries <[email protected]>2025-09-11 00:48:15 +0200
committerGitHub <[email protected]>2025-09-11 00:48:15 +0200
commit0c69c9848c6b98c92609d209103efc60003c9723 (patch)
tree445121def885f13d0f80c06864ad2ff4d5ef95a1 /src/routes/(app)/settings
parent2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff)
parent9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff)
downloadcoinos-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.svelte30
-rw-r--r--src/routes/(app)/settings/account/+page.server.ts8
-rw-r--r--src/routes/(app)/settings/account/+page.svelte407
-rw-r--r--src/routes/(app)/settings/nostr/+page.svelte39
-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.svelte181
-rw-r--r--src/routes/(app)/settings/security/+page.svelte31
-rw-r--r--src/routes/(app)/settings/square/+page.server.ts18
-rw-r--r--src/routes/(app)/settings/square/+page.svelte0
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