Migrates to svelte 5

This commit is contained in:
Christian Beutel
2025-01-24 17:46:13 +01:00
parent 7571f6e9b9
commit 3721b7f537
106 changed files with 5524 additions and 4232 deletions

View File

@@ -1,8 +1,14 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import type { SelectItem } from "$lib/components/base/select.svelte";
import { _ } from "svelte-i18n";
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
}
let { children }: Props = $props();
const settingsLinks: SelectItem[] = [
{ text: $_("profile"), value: "/settings/profile" },
@@ -22,11 +28,12 @@
},
];
$: activeIndex = settingsLinks.findIndex(
(l) => l.value === $page.url.pathname,
let activeIndex = $derived(
settingsLinks.findIndex((l) => l.value === page.url.pathname),
);
function handleItemClick(item: SelectItem) {
function handleItemClick(e: Event, item: SelectItem) {
e.stopPropagation();
if (item.value != settingsLinks.at(-1)?.value) {
goto(item.value);
} else {
@@ -44,13 +51,13 @@
class="menu-item flex items-center px-4 py-3 my-1 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors rounded-md"
class:bg-menu-item-background-hover={i == activeIndex}
role="presentation"
on:mousedown|stopPropagation={() => handleItemClick(item)}
onmousedown={(e) => handleItemClick(e, item)}
>
{item.text}
</li>
{/each}
</menu>
<div class="settings-content">
<slot></slot>
{@render children?.()}
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import ConfirmModal from "$lib/components/confirm_modal.svelte";
import EmailModal from "$lib/components/settings/email_modal.svelte";
import PasswordModal from "$lib/components/settings/password_modal.svelte";
@@ -14,16 +14,16 @@
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
const settings = $page.data.settings;
const settings = page.data.settings;
let selectedLanguage = "en";
let selectedMapFocus = "trails";
let citySearchQuery: string = "";
let openConfirmModal: () => void;
let openEmailModal: () => void;
let openPasswordModal: () => void;
let confirmModal: ConfirmModal;
let emailModal: EmailModal;
let passwordModal: PasswordModal;
onMount(() => {
citySearchQuery = settings?.location?.name ?? "";
@@ -84,33 +84,37 @@
<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" on:click={openEmailModal}
>{$_("change-email")}</button
<button
class="btn-secondary block"
onclick={() => emailModal.openModal()}>{$_("change-email")}</button
>
<button class="btn-secondary" on:click={openPasswordModal}
<button class="btn-secondary" onclick={() => passwordModal.openModal()}
>{$_("change-password")}</button
>
<div class="space-y-4">
<h4 class="text-xl text-red-400 font-medium">
{$_("danger-zone")}
</h4>
<button id="delete-account" class="btn-danger" on:click={openConfirmModal}
<button
id="delete-account"
class="btn-danger"
onclick={() => confirmModal.openModal()}
>{$_("delete-account")}</button
>
</div>
</div>
<EmailModal
email={$currentUser.email}
on:save={(e) => updateEmail(e.detail)}
bind:openModal={openEmailModal}
onsave={updateEmail}
bind:this={emailModal}
></EmailModal>
<PasswordModal
on:save={(e) => updatePassword(e.detail)}
bind:openModal={openPasswordModal}
onsave={updatePassword}
bind:this={passwordModal}
></PasswordModal>
{/if}
<ConfirmModal
text={$_("account-delete-confirm")}
bind:openModal={openConfirmModal}
on:confirm={deleteAccount}
bind:this={confirmModal}
onconfirm={deleteAccount}
></ConfirmModal>

View File

@@ -13,22 +13,22 @@
import JSZip from "jszip";
import { _ } from "svelte-i18n";
import { linear } from "svelte/easing";
import { tweened } from "svelte/motion";
import { Tween } from "svelte/motion";
import emptyStateUploadDark from "$lib/assets/svgs/empty_states/empty_state_upload_dark.svg";
import emptyStateUploadLight from "$lib/assets/svgs/empty_states/empty_state_upload_light.svg";
import { theme } from "$lib/stores/theme_store";
import { onMount } from "svelte";
import JSConfetti from "js-confetti";
const uploadProgress = tweened(0, {
const uploadProgress = new Tween(0, {
duration: 300,
easing: linear,
});
let openExportModal: () => void;
let exportModal: TrailExportModal;
let offerUpload: boolean = false;
let uploading: boolean = false;
let offerUpload: boolean = $state(false);
let uploading: boolean = $state(false);
let jsConfetti: JSConfetti;
@@ -98,7 +98,7 @@
uploading = false;
jsConfetti.addConfetti({
confettiRadius: 4,
emojis: ['🍃', '🍁'],
emojis: ["🍃", "🍁"],
});
}, 500);
@@ -191,13 +191,18 @@
: 'border border-content border-dashed'} rounded-xl flex items-center justify-center text-gray-500 bg-background cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
class:bg-menu-item-background-hover={uploading}
class:border-2={offerUpload && !uploading}
style="--progress: {$uploadProgress}%"
on:click={openFileBrowser}
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
on:drop={handleDrop}
style="--progress: {uploadProgress.current}%"
onclick={openFileBrowser}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
ondrop={handleDrop}
>
<canvas id="confetti-canvas" class="absolute" style="width: 100%; height: 200%"> </canvas>
<canvas
id="confetti-canvas"
class="absolute"
style="width: 100%; height: 200%"
>
</canvas>
<div class="">
<img
class="rounded-full aspect-square mx-auto"
@@ -216,18 +221,18 @@
accept=".gpx,.GPX,.tcx,.TCX,.kml,.KML,.fit,.FIT"
multiple={true}
style="display: none;"
on:change={() => handleFileSelection()}
onchange={() => handleFileSelection()}
/>
<h3 class="text-2xl font-semibold">{$_("export")}</h3>
<hr class="mt-4 mb-6 border-input-border" />
<Button secondary={true} on:click={() => openExportModal()}
<Button secondary={true} onclick={() => exportModal.openModal()}
>{$_("export-all-trails")}</Button
>
</div>
<TrailExportModal
bind:openModal={openExportModal}
on:export={(e) => exportTrails(e.detail)}
bind:this={exportModal}
onexport={exportTrails}
></TrailExportModal>
<style>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import RadioGroup, {
type RadioItem,
} from "$lib/components/base/radio_group.svelte";
@@ -10,7 +10,7 @@
import { settings_update } from "$lib/stores/settings_store";
import { _, locale } from "svelte-i18n";
const settings = $page.data.settings;
const settings = page.data.settings;
const languages: SelectItem[] = [
{ text: $_("chinese"), value: "zh" },
@@ -30,7 +30,7 @@
{ text: $_("imperial"), value: "imperial" },
];
let selectedLanguage = settings?.language ?? "en";
let selectedLanguage = $state(settings?.language ?? "en");
async function handleLanguageSelection(
value: Language,
@@ -59,12 +59,12 @@
<Select
items={languages}
bind:value={selectedLanguage}
on:change={(e) => handleLanguageSelection(e.detail)}
onchange={handleLanguageSelection}
></Select>
<h4 class="text-xl font-medium mt-6 mb-2">{$_("units")}</h4>
<RadioGroup
name="unit"
items={units}
selected={settings?.unit == "metric" ? 0 : 1}
on:change={(e) => handleUnitSelection(e.detail)}
onchange={handleUnitSelection}
></RadioGroup>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
@@ -14,7 +14,7 @@
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
$: settings = $page.data.settings;
let settings = $derived(page.data.settings);
const mapFocus: SelectItem[] = [
{ text: $_("trail", { values: { n: 2 } }), value: "trails" },
@@ -22,15 +22,15 @@
];
let selectedLanguage = "en";
let selectedMapFocus = "trails";
let selectedMapFocus = $state("trails");
let searchDropdownItems: SearchItem[] = [];
let citySearchQuery: string = "";
let searchDropdownItems: SearchItem[] = $state([]);
let citySearchQuery: string = $state("");
let customTilesetName: string = "";
let customTilesetURL: string = "";
let terrainURL: string = "";
let hillshadingURL: string = "";
let customTilesetName: string = $state("");
let customTilesetURL: string = $state("");
let terrainURL: string = $state("");
let hillshadingURL: string = $state("");
onMount(() => {
citySearchQuery = settings?.location?.name ?? "";
@@ -106,9 +106,10 @@
});
}
$: terrainSaveEnabled =
let terrainSaveEnabled = $derived(
terrainURL !== settings?.terrain?.terrain ||
hillshadingURL !== settings?.terrain?.hillshading;
hillshadingURL !== settings?.terrain?.hillshading,
);
</script>
<svelte:head>
@@ -123,7 +124,7 @@
<Select
items={mapFocus}
bind:value={selectedMapFocus}
on:change={(e) => handleMapFocusSelection(e.detail)}
onchange={handleMapFocusSelection}
></Select>
{#if selectedMapFocus == "location"}
<div class="mt-3">
@@ -132,8 +133,8 @@
placeholder="{$_('search-cities')}..."
clearAfterSelect={false}
bind:value={citySearchQuery}
on:update={(e) => searchCities(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
onupdate={searchCities}
onclick={handleSearchClick}
></Search>
</div>
{/if}
@@ -150,8 +151,9 @@
<p class="text-sm text-gray-500">{tileset.url}</p>
</div>
<button
aria-label="Delete tileset"
class="btn-icon"
on:click={() => handleTilesetDelete(i)}
onclick={() => handleTilesetDelete(i)}
><i class="fa fa-trash text-red-500"></i></button
>
</div>
@@ -170,7 +172,10 @@
placeholder="https://.../style.json"
></TextField>
</div>
<button class="btn-icon mt-6" on:click={handleTilesetAdd}
<button
aria-label="Add tileset"
class="btn-icon mt-6"
onclick={handleTilesetAdd}
><i class="fa fa-plus"></i></button
>
</div>
@@ -195,10 +200,11 @@
></TextField>
</div>
<button
aria-label="Save terrain settings"
disabled={!terrainSaveEnabled}
class="btn-icon mt-6"
class:hover:!bg-background={!terrainSaveEnabled}
on:click={handleTerrainAdd}
onclick={handleTerrainAdd}
class:text-gray-500={!terrainSaveEnabled}
><i class="fa fa-save"></i></button
>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import Toggle from "$lib/components/base/toggle.svelte";
import { NotificationType } from "$lib/models/notification";
import type { Settings } from "$lib/models/settings";
import { settings_update } from "$lib/stores/settings_store";
import { _ } from "svelte-i18n";
const notifications = ($page.data.settings as Settings)?.notifications ?? {
const notifications = $state((page.data.settings as Settings)?.notifications ?? {
list_create: {
web: true,
email: true,
@@ -31,7 +31,7 @@
web: true,
email: true,
},
};
});
const notificationItems: { text: string; key: NotificationType }[] = [
{
@@ -62,7 +62,7 @@
async function updateNotificationSettings() {
await settings_update({
id: $page.data.settings!.id,
id: page.data.settings!.id,
notifications,
});
}
@@ -85,13 +85,13 @@
<p>{item.text}</p>
<div>
<Toggle
on:change={updateNotificationSettings}
onchange={updateNotificationSettings}
bind:value={notifications[item.key].web}
></Toggle>
</div>
<div>
<Toggle
on:change={updateNotificationSettings}
onchange={updateNotificationSettings}
bind:value={notifications[item.key].email}
></Toggle>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import RadioGroup, {
type RadioItem,
} from "$lib/components/base/radio_group.svelte";
@@ -7,7 +7,7 @@
import { settings_update } from "$lib/stores/settings_store";
import { _ } from "svelte-i18n";
$: settings = $page.data.settings as Settings;
let settings = $derived(page.data.settings as Settings);
const accoutPrivacyItems: RadioItem[] = [
{
@@ -93,7 +93,7 @@
name="account"
items={accoutPrivacyItems}
selected={settings?.privacy?.account == "private" ? 1 : 0}
on:change={(e) => handleAccountSelection(e.detail)}
onchange={handleAccountSelection}
></RadioGroup>
<h4 class="text-xl font-medium mt-12 mb-4">
@@ -103,7 +103,7 @@
name="trails"
items={trailPrivacyItems}
selected={settings?.privacy?.trails == "public" ? 0 : 1}
on:change={(e) => handleTrailsSelection(e.detail)}
onchange={handleTrailsSelection}
></RadioGroup>
<h4 class="text-xl font-medium mt-12 mb-4">
@@ -113,5 +113,5 @@
name="lists"
items={listPrivacyItems}
selected={settings?.privacy?.lists == "public" ? 0 : 1}
on:change={(e) => handleListsSelection(e.detail)}
onchange={handleListsSelection}
></RadioGroup>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import Button from "$lib/components/base/button.svelte";
import type { SelectItem } from "$lib/components/base/select.svelte";
import Select from "$lib/components/base/select.svelte";
@@ -11,12 +11,15 @@
import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n";
export let data;
let { data } = $props();
let selectedCategory =
$page.data.settings?.category || data.categories[0].id;
let settings = $state(data.settings)
let bio = data.settings?.bio ?? "";
let selectedCategory = $state(
data.settings?.category || data.categories[0].id,
);
let bio = $state(data.settings?.bio ?? "");
const categoryItems: SelectItem[] = data.categories.map((c: Category) => ({
text: $_(c.name),
@@ -43,12 +46,12 @@
}
async function handleBioSave() {
if (!data.settings) {
if (!settings) {
return;
}
try {
data.settings.bio = bio;
await settings_update(data.settings);
settings.bio = bio;
await settings_update(settings);
} catch (e) {
show_toast({
type: "error",
@@ -61,7 +64,7 @@
async function handleCategorySelection(value: string) {
await settings_update({
id: $page.data.settings!.id,
id: settings.id,
category: value,
});
}
@@ -85,8 +88,9 @@
alt="avatar"
/>
<button
aria-label="Open file browser"
class="absolute top-0 w-24 aspect-square opacity-0 group-hover:opacity-100 flex justify-center items-center bg-black/50 focus:bg-black/60 text-white cursor-pointer transition-opacity"
on:click={openFileBrowser}
onclick={openFileBrowser}
>
<i class="fa fa-pen"></i>
</button>
@@ -96,7 +100,7 @@
id="avatarInput"
accept="image/*"
style="display: none;"
on:change={handleAvatarSelection}
onchange={handleAvatarSelection}
/>
</div>
<div>
@@ -109,9 +113,9 @@
<Textarea bind:value={bio} rows={5}></Textarea>
<div class="mt-3">
<Button
on:click={() => handleBioSave()}
onclick={() => handleBioSave()}
primary
disabled={data.settings.bio === bio}>{$_("save")}</Button
disabled={settings.bio === bio}>{$_("save")}</Button
>
</div>
</div>
@@ -121,7 +125,7 @@
<Select
items={categoryItems}
bind:value={selectedCategory}
on:change={(e) => handleCategorySelection(e.detail)}
onchange={handleCategorySelection}
></Select>
</div>
</div>