Files
wanderer/web/src/routes/settings/account/+page.svelte
slothful-vassal 1b2781960a fix: resolve Svelte 5 reactivity warnings (#974)
- Use untrack() for $derived/$state initial values captured outside reactive context (scene.svelte, lists edit page)
- Add $state() to bind:this component references in settings account page

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
2026-05-10 08:42:20 +02:00

250 lines
8.9 KiB
Svelte

<script lang="ts">
import { goto, invalidateAll } from "$app/navigation";
import { page } from "$app/state";
import Button from "$lib/components/base/button.svelte";
import ConfirmModal from "$lib/components/confirm_modal.svelte";
import ApiTokenModal from "$lib/components/settings/api_token_modal.svelte";
import ApiTokenSuccessModal from "$lib/components/settings/api_token_success_modal.svelte";
import EmailModal from "$lib/components/settings/email_modal.svelte";
import PasswordModal from "$lib/components/settings/password_modal.svelte";
import type { APIToken } from "$lib/models/api_token";
import {
api_tokens_create,
api_tokens_delete,
} from "$lib/stores/api_token_store";
import { show_toast } from "$lib/stores/toast_store.svelte";
import {
currentUser,
logout,
users_delete,
users_update,
users_update_email,
} from "$lib/stores/user_store";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let { data } = $props();
const settings = page.data.settings;
let selectedLanguage = "en";
let selectedMapFocus = "trails";
let citySearchQuery: string = "";
let confirmModal: ConfirmModal = $state()!;
let emailModal: EmailModal = $state()!;
let passwordModal: PasswordModal = $state()!;
let tokenModal: ApiTokenModal = $state()!;
let tokenSuccessModal: ApiTokenSuccessModal = $state()!;
let tokenLoading: boolean = $state(false);
let rawAPIToken: string | null = $state(null);
onMount(() => {
citySearchQuery = settings?.location?.name ?? "";
selectedLanguage = settings?.language || "en";
selectedMapFocus = settings?.mapFocus ?? "trails";
});
async function deleteAccount() {
await users_delete($currentUser!);
logout();
goto("/");
}
async function updateEmail(email: string, currentPassword: string) {
try {
await users_update_email($currentUser!.id!, email, currentPassword);
show_toast({
text: $_("email-updated"),
icon: "check",
type: "success",
});
} catch (e) {
show_toast({
text: "Error updating email",
icon: "close",
type: "error",
});
}
}
async function updatePassword(data: {
oldPassword: string;
password: string;
passwordConfirm: string;
}) {
try {
await users_update({ ...$currentUser!, ...data });
show_toast({
text: $_("password-updated"),
icon: "check",
type: "success",
});
} catch (e) {
show_toast({
text: $_("error-updating-password"),
icon: "close",
type: "error",
});
}
}
async function generateAPIToken(token: APIToken) {
try {
tokenLoading = true;
const tokenResponse = await api_tokens_create(token);
rawAPIToken = tokenResponse.rawToken;
tokenSuccessModal.openModal();
await invalidateAll();
} catch (e) {
show_toast({
text: $_("error-generating-token"),
icon: "close",
type: "error",
});
} finally {
tokenLoading = false;
}
}
async function deleteAPIToken(token: APIToken) {
try {
const tokenResponse = await api_tokens_delete(token);
await invalidateAll();
} catch (e) {
show_toast({
text: $_("error-deleting-token"),
icon: "close",
type: "error",
});
}
}
</script>
<svelte:head>
<title>{$_("settings")} | wanderer</title>
</svelte:head>
{#if $currentUser}
<h2 class="text-2xl font-semibold">{$_("my-account")}</h2>
<hr class="mt-4 mb-6 border-input-border" />
<div class="space-y-6">
<h4 class="text-xl font-medium">{$_("login-details")}</h4>
<button
class="btn-secondary block"
onclick={() => emailModal.openModal()}>{$_("change-email")}</button
>
<button class="btn-secondary" onclick={() => passwordModal.openModal()}
>{$_("change-password")}</button
>
<div>
<div class="flex justify-between items-center">
<h4 class="text-xl font-medium">{$_("api-tokens")}</h4>
<Button
secondary
onclick={() => tokenModal.openModal()}
loading={tokenLoading}
><i class="fa fa-plus mr-2"></i>
{$_("generate-new-token")}</Button
>
</div>
<p class="mt-3">{$_("api-tokens-hint")}</p>
{#if data.apiTokens.totalItems == 0}
<p class="text-center pt-8 pb-2 text-gray-500">
{$_("no-api-tokens")}
</p>
{:else}
<div
class="border border-input-border rounded-xl overflow-clip mt-4"
>
<table class="api-token-table w-full table-auto">
<thead class="text-left">
<tr class="text-sm bg-secondary-hover">
<th>{$_("name")}</th>
<th>{$_("expiration")}</th>
<th>{$_("last-used")}</th>
<th></th>
</tr>
</thead>
<tbody>
{#each data.apiTokens.items as token}
<tr class="border-t border-input-border">
<td>{token.name}</td>
<td
>{token.expiration
? new Date(
token.expiration,
).toLocaleDateString(undefined, {
month: "long",
day: "2-digit",
year: "numeric",
timeZone: "UTC",
})
: $_("never")}</td
>
<td
>{token.last_used
? new Date(
token.last_used,
).toLocaleTimeString(undefined, {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})
: "-"}</td
>
<td
><button
onclick={() =>
deleteAPIToken(token)}
aria-label="delete api token"
><i class="fa fa-trash"></i></button
></td
>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
<div class="space-y-4">
<h4 class="text-xl text-red-400 font-medium">
{$_("danger-zone")}
</h4>
<button
id="delete-account"
class="btn-danger"
onclick={() => confirmModal.openModal()}
>{$_("delete-account")}</button
>
</div>
</div>
<EmailModal
email={$currentUser.email}
onsave={updateEmail}
bind:this={emailModal}
></EmailModal>
<PasswordModal onsave={updatePassword} bind:this={passwordModal}
></PasswordModal>
<ApiTokenModal onsave={generateAPIToken} bind:this={tokenModal}
></ApiTokenModal>
<ApiTokenSuccessModal bind:token={rawAPIToken} bind:this={tokenSuccessModal}
></ApiTokenSuccessModal>
{/if}
<ConfirmModal
text={$_("account-delete-confirm")}
bind:this={confirmModal}
onconfirm={deleteAccount}
></ConfirmModal>
<style>
.api-token-table th,
td {
padding: 16px;
}
</style>