aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/settings/account
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
commit5d4c882cc48bbee005ba65f082ad7bd771070b3d (patch)
tree54822d1f2cbc4c3672ab67b2eb2585e6af88e6cb /src/routes/(app)/settings/account
parent9d66feae5483141be0689d834b6d45906d6be84a (diff)
downloadcoinos-ui-5d4c882cc48bbee005ba65f082ad7bd771070b3d.tar.gz
coinos-ui-5d4c882cc48bbee005ba65f082ad7bd771070b3d.zip
settings re-org
Diffstat (limited to 'src/routes/(app)/settings/account')
-rw-r--r--src/routes/(app)/settings/account/+page.server.ts6
-rw-r--r--src/routes/(app)/settings/account/+page.svelte377
2 files changed, 252 insertions, 131 deletions
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}