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,12 +1,12 @@
<script>
import { page } from "$app/stores";
import { page } from "$app/state";
import errorDark from "$lib/assets/svgs/empty_states/error_dark.svg";
import errorLight from "$lib/assets/svgs/empty_states/error_light.svg";
import { theme } from "$lib/stores/theme_store";
</script>
<svelte:head>
<title>{$page.status} | wanderer</title>
<title>{page.status} | wanderer</title>
</svelte:head>
<div
class="m-auto text-center max-w-md"
@@ -20,10 +20,10 @@
/>
<div class="text-center mt-8">
<h1 class="font-bold" style="line-height: 4rem;">
{$page.status}
{page.status}
</h1>
<p class="text-lg text-center mt-2">
{$page.error?.message}
{page.error?.message}
</p>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { beforeNavigate, goto } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import { env } from "$env/dynamic/public";
import Toast from "$lib/components/base/toast.svelte";
import Footer from "$lib/components/footer.svelte";
@@ -8,12 +8,17 @@
import { currentUser } from "$lib/stores/user_store";
import { isRouteProtected } from "$lib/util/authorization_util";
import "@fortawesome/fontawesome-free/css/all.min.css";
import { onMount } from "svelte";
import { onMount, type Snippet } from "svelte";
import { slide } from "svelte/transition";
import "../css/app.css";
import "../css/components.css";
import "../css/theme.css";
import PageLoadingBar from "$lib/components/page_loading_bar.svelte";
interface Props {
children?: Snippet;
}
let { children }: Props = $props();
beforeNavigate((n) => {
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
@@ -23,13 +28,13 @@
});
onMount(() => {
if ($page.data.origin != location.origin) {
if (page.data.origin != location.origin) {
showWarning = true;
}
});
let hideDemoHint = false;
let showWarning = false;
let hideDemoHint = $state(false);
let showWarning = $state(false);
</script>
{#if env.PUBLIC_IS_DEMO === "true" && !hideDemoHint}
@@ -42,7 +47,10 @@
>This is a demo instance. Do not store any relevant data here. You
can use the user 'demo' and password 'password' to login.
</span>
<button class="btn-icon self-end" on:click={() => (hideDemoHint = true)}
<button
aria-label="Close"
class="btn-icon self-end"
onclick={() => (hideDemoHint = true)}
><i class="fa fa-close"></i></button
>
</div>
@@ -59,10 +67,12 @@
>{location.origin}</span
>
but your ORIGIN environment variable is set to
<span class="font-mono bg-gray-100">{$page.data.origin}</span>. This
<span class="font-mono bg-gray-100">{page.data.origin}</span>. This
may cause errors.
</p>
<button class="btn-icon self-end" on:click={() => (showWarning = false)}
<button
aria-label="Close"
class="btn-icon self-end" onclick={() => (showWarning = false)}
><i class="fa fa-close"></i></button
>
</div>
@@ -71,6 +81,6 @@
<NavBar></NavBar>
<PageLoadingBar class="text-content"></PageLoadingBar>
<Toast></Toast>
<slot />
{@render children?.()}
<Footer></Footer>

View File

@@ -15,9 +15,9 @@
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import { theme } from "$lib/stores/theme_store";
export let data;
let { data } = $props();
let searchDropdownItems: SearchItem[] = [];
let searchDropdownItems: SearchItem[] = $state([]);
async function search(q: string) {
const r = await fetch("/api/v1/search/multi", {
@@ -92,15 +92,15 @@
{$_("hero_section_0_text")}
</h5>
<Search
on:update={(e) => search(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
onupdate={search}
onclick={handleSearchClick}
large={true}
placeholder="{$_('search-for-trails-places')}..."
items={searchDropdownItems}
></Search>
</div>
<div class="hidden md:block">
<Canvas>
<Canvas toneMapping={0}>
<Scene></Scene>
</Canvas>
</div>

View File

@@ -9,7 +9,9 @@ export async function GET(event: RequestEvent) {
min_distance: 0,
max_distance: 20000,
min_elevation_gain: 0,
max_elevation_gain: 4000
max_elevation_gain: 4000,
min_elevation_loss: 0,
max_elevation_loss: 4000
});
}
try {

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 Button from "$lib/components/base/button.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
@@ -13,7 +13,7 @@
import { _ } from "svelte-i18n";
import { z } from "zod";
let loading: boolean = false;
let loading: boolean = $state(false);
const { form, errors } = createForm({
initialValues: {
@@ -46,7 +46,7 @@
try {
await users_confirm_reset({
...form,
token: $page.params.token,
token: page.params.token,
});
goto("/login");
show_toast({

View File

@@ -12,7 +12,7 @@
import { _ } from "svelte-i18n";
import { z } from "zod";
let loading: boolean = false;
let loading: boolean = $state(false);
const { form, errors } = createForm({
initialValues: {

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 type { DropdownItem } from "$lib/components/base/dropdown.svelte";
import Search, {
type SearchItem,
@@ -17,7 +17,7 @@
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte";
import UserSearch from "$lib/components/user_search.svelte";
import { List } from "$lib/models/list";
import { List, type ListFilter } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import { lists_delete, lists_index } from "$lib/stores/list_store";
import { fetchGPX } from "$lib/stores/trail_store";
@@ -28,7 +28,7 @@
import { _ } from "svelte-i18n";
import { slide } from "svelte/transition";
export let data;
let { data } = $props();
const sortOptions: SelectItem[] = [
{ text: $_("alphabetical"), value: "name" },
@@ -40,35 +40,43 @@
totalPages: data.lists.totalPages,
};
let openConfirmModal: () => void;
let openShareModal: () => void;
let lists = $state(data.lists);
let map: M.Map;
let mapWithElevation: MapWithElevationMaplibre;
let markers: M.Marker[];
let confirmModal: ConfirmModal;
let listShareModal: ListShareModal;
let filter: ListFilter = $state(page.data.filter);
let map: M.Map | undefined = $state();
let mapWithElevation: MapWithElevationMaplibre | undefined = $state();
let markers: M.Marker[] = $state([]);
let showMap: boolean = true;
let selectedList: List | null = $page.url.searchParams.get("list")
? data.lists.items[0]
: null;
let selectedTrail: Trail | null = null;
let selectedList: List | null = $state(
page.url.searchParams.get("list") ? lists.items[0] : null,
);
let selectedTrail: Trail | null = $state(null);
let loading: boolean = false;
let loading: boolean = $state(false);
let loadingNextPage: boolean = false;
let filterExpanded: boolean = false;
let filterExpanded: boolean = $state(false);
let loadAllListsOnNextBack = false;
let userQuery = "";
let userQuery = $state("");
$: selectedTrailIndex = selectedTrail
? (selectedList?.expand?.trails?.indexOf(selectedTrail) ?? null)
: null;
let selectedTrailIndex = $derived(
selectedTrail
? (selectedList?.expand?.trails?.indexOf(selectedTrail) ?? null)
: null,
);
$: selectedTrailWaypoints = selectedTrail?.expand?.waypoints;
let selectedTrailWaypoints = $derived(
(selectedTrail as Trail | null)?.expand?.waypoints,
);
onMount(() => {
if ($page.url.searchParams.get("list") && selectedList) {
if (page.url.searchParams.get("list") && selectedList) {
setCurrentList(selectedList);
// only the requested list has been loaded at this point
@@ -77,18 +85,16 @@
}
});
async function handleDropdownClick(e: CustomEvent<DropdownItem>) {
const item = e.detail;
async function handleDropdownClick(item: DropdownItem) {
if (!selectedList) {
return;
}
if (item.value == "share") {
openShareModal();
listShareModal.openModal();
} else if (item.value == "edit") {
goto("/lists/edit/" + selectedList.id);
} else if (item.value == "delete") {
openConfirmModal();
confirmModal.openModal();
}
}
@@ -128,7 +134,7 @@
selectedTrail = null;
} else if (selectedList) {
selectedList = null;
map.flyTo({
map?.flyTo({
animate: true,
zoom: 1,
center: [0, 0],
@@ -138,16 +144,16 @@
function selectTrail(trail: Trail) {
selectedTrail = trail;
mapWithElevation.unHighlightTrail(trail.id!);
mapWithElevation?.unHighlightTrail(trail.id!);
window.scrollTo({ top: 0 });
}
function highlightTrail(trail: Trail) {
mapWithElevation.highlightTrail(trail.id!);
mapWithElevation?.highlightTrail(trail.id!);
}
function unHighlightTrail(trail: Trail) {
mapWithElevation.unHighlightTrail(trail.id!);
mapWithElevation?.unHighlightTrail(trail.id!);
}
async function onListScroll(e: Event) {
@@ -169,7 +175,7 @@
async function loadNextPage() {
pagination.page += 1;
data.lists = await lists_index(data.filter, pagination.page);
lists = await lists_index(filter, pagination.page);
}
async function updateFilter() {
@@ -178,7 +184,7 @@
if (selectedList || selectedTrail) {
selectedList = null;
selectedTrail = null;
map.flyTo({
map?.flyTo({
animate: true,
zoom: 1,
center: [0, 0],
@@ -186,41 +192,41 @@
}
pagination.page = 0;
data.lists = await lists_index(data.filter, pagination.page);
lists = await lists_index(filter, pagination.page);
loading = false;
}
async function setSort(value: "name" | "created") {
data.filter.sort = value;
filter.sort = value;
await updateFilter();
}
async function setSortOrder() {
if (data.filter.sortOrder === "+") {
data.filter.sortOrder = "-";
if (filter.sortOrder === "+") {
filter.sortOrder = "-";
} else {
data.filter.sortOrder = "+";
filter.sortOrder = "+";
}
await updateFilter();
}
async function setPublicFilter(e: Event) {
data.filter.public = (e.target as HTMLInputElement).checked;
filter.public = (e.target as HTMLInputElement).checked;
updateFilter();
}
async function setAuthorFilter(item: SearchItem) {
data.filter.author = item.value.id;
filter.author = item.value.id;
await updateFilter();
}
async function clearAuthorFilter() {
data.filter.author = "";
filter.author = "";
await updateFilter();
}
async function setSharedFilter(e: Event) {
data.filter.shared = (e.target as HTMLInputElement).checked;
filter.shared = (e.target as HTMLInputElement).checked;
updateFilter();
}
</script>
@@ -236,19 +242,21 @@
class="flex gap-x-3 items-center px-3 py-4 bg-background z-50 rounded-xl"
>
<button
aria-label="Back"
class="btn-icon"
class:btn-disabled={!selectedList}
disabled={!selectedList}
on:click={back}><i class="fa fa-arrow-left"></i></button
onclick={back}><i class="fa fa-arrow-left"></i></button
>
<Search bind:value={data.filter.q} on:update={updateFilter}
></Search>
<Search bind:value={filter.q} onupdate={updateFilter}></Search>
<button
aria-label="Toggle filter"
class="btn-icon"
on:click={() => (filterExpanded = !filterExpanded)}
onclick={() => (filterExpanded = !filterExpanded)}
><i class="fa fa-sliders"></i></button
>
<a
aria-label="New list"
class="btn-primary tooltip"
data-title={$_("new-list")}
href="/lists/edit/new"><i class="fa fa-plus"></i></a
@@ -263,15 +271,16 @@
<p class="text-sm font-medium pb-1">{$_("sort")}</p>
<div class="flex items-center gap-2">
<Select
bind:value={data.filter.sort}
bind:value={filter.sort}
items={sortOptions}
on:change={(e) => setSort(e.detail)}
onchange={setSort}
></Select>
<button
aria-label="Set sort order"
id="sort-order-btn"
class="btn-icon"
class:rotated={data.filter.sortOrder == "-"}
on:click={() => setSortOrder()}
class:rotated={filter.sortOrder == "-"}
onclick={() => setSortOrder()}
><i class="fa fa-arrow-up"></i></button
>
</div>
@@ -279,8 +288,8 @@
<hr class="my-4 border-separator" />
<UserSearch
on:click={(e) => setAuthorFilter(e.detail)}
on:clear={clearAuthorFilter}
onclick={setAuthorFilter}
onclear={clearAuthorFilter}
bind:value={userQuery}
clearAfterSelect={false}
label={$_("author")}
@@ -289,9 +298,9 @@
<input
id="public-checkbox"
type="checkbox"
checked={data.filter.public}
checked={filter.public}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={setPublicFilter}
onchange={setPublicFilter}
/>
<label for="public-checkbox" class="ms-2 text-sm"
>{$_("public")}</label
@@ -301,9 +310,9 @@
<input
id="shared-checkbox"
type="checkbox"
checked={data.filter.shared}
checked={filter.shared}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={setSharedFilter}
onchange={setSharedFilter}
/>
<label for="shared-checkbox" class="ms-2 text-sm"
>{$_("shared")}</label
@@ -317,7 +326,7 @@
<div
id="list-container"
class="overflow-y-scroll overflow-x-clip max-h-full"
on:scroll={onListScroll}
onscroll={onListScroll}
>
{#if !selectedList}
<div class="px-4 mt-2" class:space-y-3={loading}>
@@ -325,13 +334,13 @@
{#each { length: 3 } as _, index}
<SkeletonCard></SkeletonCard>
{/each}
{:else if data.lists.items.length == 0}
{:else if lists.items.length == 0}
<EmptyStateSearch width={356}></EmptyStateSearch>
{:else}
{#each data.lists.items as item, i}
{#each lists.items as item, i}
<div
class="list-list-item"
on:click={() => setCurrentList(item)}
onclick={() => setCurrentList(item)}
role="presentation"
>
<ListCard list={item}></ListCard>
@@ -342,10 +351,10 @@
{:else if selectedList && !selectedTrail}
<ListPanel
list={selectedList}
on:mouseenter={(e) => highlightTrail(e.detail.trail)}
on:mouseleave={(e) => unHighlightTrail(e.detail.trail)}
on:change={(e) => handleDropdownClick(e)}
on:click={(e) => selectTrail(e.detail.trail)}
onmouseenter={(data) => highlightTrail(data.trail)}
onmouseleave={(data) => unHighlightTrail(data.trail)}
onchange={(item) => handleDropdownClick(item)}
onclick={(data) => selectTrail(data.trail)}
></ListPanel>
{:else if selectedList && selectedTrail}
<TrailInfoPanel trail={selectedTrail} mode="list" {markers}
@@ -362,8 +371,8 @@
bind:markers
activeTrail={selectedTrailIndex}
fitBounds="animate"
on:select={(e) => {
selectedTrail = e.detail;
onselect={(trail) => {
selectedTrail = trail;
}}
showInfoPopup={true}
showTerrain={true}
@@ -375,17 +384,11 @@
<ConfirmModal
text={$_("delete-list-confirm")}
bind:openModal={openConfirmModal}
on:confirm={deleteList}
bind:this={confirmModal}
onconfirm={deleteList}
></ConfirmModal>
{#if selectedList}
<ListShareModal
list={selectedList}
bind:openShareModal
on:update={() => {
data.lists.items = data.lists.items;
selectedList = selectedList;
}}
<ListShareModal bind:this={listShareModal} list={selectedList}
></ListShareModal>
{/if}
</main>

View File

@@ -2,7 +2,7 @@
import { type List } from "$lib/models/list";
import { _ } from "svelte-i18n";
import { page } from "$app/stores";
import { page } from "$app/state";
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import Button from "$lib/components/base/button.svelte";
@@ -38,21 +38,21 @@
import { createForm } from "felte";
import { z } from "zod";
export let data;
let { data } = $props();
let previewURL = data.previewUrl ?? "";
let searchDropdownItems: SearchItem[] = [];
let previewURL = $state(data.previewUrl ?? "");
let searchDropdownItems: SearchItem[] = $state([]);
let activeTrailIndex: number = -1;
let activeTrailIndex: number = $state(-1);
let map: MapWithElevationMaplibre;
let map: MapWithElevationMaplibre | undefined = $state();
let loading: boolean = false;
let loading: boolean = $state(false);
let newShares: TrailShare[] = [];
let openConfirmModal: () => void;
let openPublishConfirmModal: () => void;
let shareConfirmModal: ConfirmModal;
let publishConfirmModal: ConfirmModal;
const ClientListCreateSchema = ListCreateSchema.extend({
_photos: z.array(z.instanceof(File)).optional(),
@@ -74,7 +74,7 @@
...data.list,
public: data.list.id
? data.list.public
: $page.data.settings?.privacy?.lists === "public",
: page.data.settings?.privacy?.lists === "public",
},
extend: validator({
schema: ClientListCreateSchema,
@@ -93,10 +93,10 @@
data.list?.expand?.trails?.length !==
$formData.expand?.trails?.length)
) {
openPublishConfirmModal();
publishConfirmModal.openModal();
return false;
} else if (await findNewTrailShares()) {
openConfirmModal();
shareConfirmModal.openModal();
return false;
}
@@ -273,7 +273,7 @@
use:form
>
<h2 class="text-2xl font-semibold">
{$page.params.id === "new" ? $_("new-list") : $_("edit-list")}
{page.params.id === "new" ? $_("new-list") : $_("edit-list")}
</h2>
<label for="avatar" class="text-sm font-medium block">
{$_("avatar")}
@@ -284,7 +284,7 @@
id="avatar"
accept="image/*"
style="display: none;"
on:change={handleAvatarSelection}
onchange={handleAvatarSelection}
/>
<div class="flex items-center gap-4">
{#if previewURL.length > 0}
@@ -301,7 +301,7 @@
<button
class="btn-secondary"
type="button"
on:click={openAvatarBrowser}>{$_("change")}...</button
onclick={openAvatarBrowser}>{$_("change")}...</button
>
</div>
@@ -318,8 +318,8 @@
{$_("trail", { values: { n: 2 } })}
</h3>
<Search
on:update={(e) => search(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
onupdate={search}
onclick={handleSearchClick}
placeholder="{$_('search-trails')}..."
items={searchDropdownItems}
></Search>
@@ -329,9 +329,8 @@
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
class:bg-secondary-hover={i == activeTrailIndex}
role="presentation"
on:mouseenter={() => map.highlightTrail(trail.id ?? "")}
on:mouseleave={() => map.unHighlightTrail(trail.id ?? "")}
on:click={() => map.selectTrail(trail.id ?? "")}
onmouseenter={() => map?.highlightTrail(trail.id ?? "")}
onmouseleave={() => map?.unHighlightTrail(trail.id ?? "")}
>
<div class="shrink-0">
<img
@@ -384,8 +383,8 @@
<div class="basis-0">
{#if i > 0}
<button
on:click|stopPropagation={() =>
moveTrail(trail, i, -1)}
aria-label="Move up"
onclick={() => moveTrail(trail, i, -1)}
type="button"
class="btn-icon"
>
@@ -394,8 +393,8 @@
{/if}
{#if i < $formData.expand?.trails?.length - 1}
<button
on:click|stopPropagation={() =>
moveTrail(trail, i, 1)}
aria-label="Move down"
onclick={() => moveTrail(trail, i, 1)}
type="button"
class="btn-icon"
>
@@ -403,9 +402,10 @@
</button>
{/if}
<button
aria-label="Remove trail"
type="button"
class="btn-icon text-red-500"
on:click={() => deleteTrail(trail)}
onclick={() => deleteTrail(trail)}
><i class="fa fa-trash"></i></button
>
</div>
@@ -426,6 +426,7 @@
</form>
<div id="trail-map" class="max-h-full">
<MapWithElevationMaplibre
fitBounds="animate"
trails={$formData.expand?.trails ?? []}
bind:activeTrail={activeTrailIndex}
bind:this={map}
@@ -438,8 +439,8 @@
text={$_("list-share-warning-update")}
title={$_("confirm-share")}
action="confirm"
bind:openModal={openConfirmModal}
on:confirm={async () => {
bind:this={shareConfirmModal}
onconfirm={async () => {
await updateTrailShares();
await saveList();
}}
@@ -450,8 +451,8 @@
text={$_("list-public-warning")}
title={$_("confirm-publish")}
action="confirm"
bind:openModal={openPublishConfirmModal}
on:confirm={async () => {
bind:this={publishConfirmModal}
onconfirm={async () => {
await publishTrails();
await saveList();
}}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import { env } from "$env/dynamic/public";
import Button from "$lib/components/base/button.svelte";
import TextField from "$lib/components/base/text_field.svelte";
@@ -16,19 +16,19 @@
import { _ } from "svelte-i18n";
import { z } from "zod";
let loading: boolean = false;
let loading: boolean = $state(false);
const authProviders = $page.data.authMethods.authProviders;
let loginLabel = "";
const authProviders = page.data.authMethods.authProviders;
let loginLabel = $state("");
if (
$page.data.authMethods.usernamePassword &&
$page.data.authMethods.emailPassword
page.data.authMethods.usernamePassword &&
page.data.authMethods.emailPassword
) {
loginLabel = `${$_("username")}/${$_("email")}`;
} else if ($page.data.authMethods.usernamePassword) {
} else if (page.data.authMethods.usernamePassword) {
loginLabel = `${$_("username")}`;
} else if ($page.data.authMethods.emailPassword) {
} else if (page.data.authMethods.emailPassword) {
loginLabel = `${$_("email")}`;
}
@@ -49,7 +49,7 @@
try {
await login(newUser);
window.location.href = $page.url.searchParams.get("r") ?? "/";
window.location.href = page.url.searchParams.get("r") ?? "/";
} catch (e) {
if (
e instanceof APIError &&
@@ -92,7 +92,7 @@
<LogoTextTwoLineLight></LogoTextTwoLineLight>
{/if}
<h4 class="text-xl font-semibold">{$_("slogan")}</h4>
{#if $page.data.authMethods.usernamePassword || $page.data.authMethods.emailPassword}
{#if page.data.authMethods.usernamePassword || page.data.authMethods.emailPassword}
<div class="space-y-6 w-80">
<TextField
name="username"
@@ -129,7 +129,7 @@
{/if}
{#if authProviders.length}
{#if $page.data.authMethods.usernamePassword || $page.data.authMethods.emailPassword}
{#if page.data.authMethods.usernamePassword || page.data.authMethods.emailPassword}
<div class="flex gap-4 items-center w-full">
<hr class="basis-full border-input-border" />
<span class="text-gray-500 uppercase">{$_("or")}</span>
@@ -141,7 +141,7 @@
<a
href={provider.url}
class="btn-secondary inline-flex min-w-full justify-center"
on:click={() => setProvider(provider)}
onclick={() => setProvider(provider)}
>
<img
class="w-5 aspect-square mr-4"

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import { oauth_login } from "$lib/stores/user_store";
import type { AuthProviderInfo } from "pocketbase";
import { onMount } from "svelte";
let error = $page.url.searchParams.get("error");
let errorDescription = $page.url.searchParams.get("error_description");
const errorURI = $page.url.searchParams.get("error_uri");
let error = $state(page.url.searchParams.get("error"));
let errorDescription = $state(page.url.searchParams.get("error_description"));
const errorURI = page.url.searchParams.get("error_uri");
const state = $page.url.searchParams.get("state");
const code = $page.url.searchParams.get("code");
const oauthState = page.url.searchParams.get("state");
const code = page.url.searchParams.get("code");
onMount(async () => {
if (error || !state || !code) {
if (error || !oauthState || !code) {
return;
}
@@ -27,7 +27,7 @@
const provider: AuthProviderInfo = JSON.parse(providerData);
if (provider.state !== state) {
if (provider.state !== oauthState) {
error = "mismacthed_provider";
errorDescription =
"OAuth provider does not match the one defined in local storage.";

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { browser } from "$app/environment";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
@@ -24,22 +24,22 @@
import { _ } from "svelte-i18n";
import { slide } from "svelte/transition";
let trails: Trail[] = [];
let trails: Trail[] = $state([]);
let map: M.Map;
let mapWithElevation: MapWithElevationMaplibre;
let searchDropdownItems: SearchItem[] = [];
let map: M.Map | undefined = $state();
let mapWithElevation: MapWithElevationMaplibre | undefined = $state();
let searchDropdownItems: SearchItem[] = $state([]);
let showFilter: boolean = false;
let showMap: boolean = true;
let showFilter: boolean = $state(false);
let showMap: boolean = $state(true);
const filter: TrailFilter = $page.data.filter;
const maxBoundingBox: TrailBoundingBox = $page.data.boundingBox;
const settings: Settings = $page.data.settings;
let filter: TrailFilter = $state(page.data.filter);
const maxBoundingBox: TrailBoundingBox = page.data.boundingBox;
const settings: Settings = page.data.settings;
const MIN_ZOOM = 6;
let loading: boolean = true;
let loading: boolean = $state(true);
onMount(async () => {});
@@ -89,8 +89,8 @@
}
function handleSearchClick(item: SearchItem) {
map.setCenter([item.value._geo.lng, item.value._geo.lat]);
map.setZoom(14);
map?.setCenter([item.value._geo.lng, item.value._geo.lat]);
map?.setZoom(14);
}
async function searchTrails(northEast: M.LngLat, southWest: M.LngLat) {
@@ -99,26 +99,32 @@
northEast,
southWest,
filter,
map.getZoom() > MIN_ZOOM,
(map?.getZoom() ?? 0) > MIN_ZOOM,
);
trails = changes.trails;
loading = false;
}
function handleTrailCardMouseEnter(trail: Trail) {
mapWithElevation.togglePopup(trail.id!, false);
mapWithElevation?.togglePopup(trail.id!, false);
}
function handleTrailCardMouseLeave(trail: Trail) {
mapWithElevation.togglePopup(trail.id!, true);
mapWithElevation?.togglePopup(trail.id!, true);
}
async function handleFilterUpdate(filter: TrailFilter) {
if (!map) {
return;
}
const bounds = map.getBounds();
await searchTrails(bounds.getNorthEast(), bounds.getSouthWest());
}
async function handleMapMove() {
if (!map) {
return;
}
const bounds = map.getBounds();
const normalizedBounds = {
@@ -130,67 +136,67 @@
((((bounds.getNorthEast().lng + 180) % 360) + 360) % 360) - 180,
bounds.getNorthEast().lat,
),
};
};
await searchTrails(
normalizedBounds.northEast,
normalizedBounds.southWest,
);
$page.url.searchParams.set("tl_lat", bounds.getNorth().toString());
$page.url.searchParams.set("tl_lon", bounds.getEast().toString());
$page.url.searchParams.set("br_lat", bounds.getSouth().toString());
$page.url.searchParams.set("br_lon", bounds.getWest().toString());
page.url.searchParams.set("tl_lat", bounds.getNorth().toString());
page.url.searchParams.set("tl_lon", bounds.getEast().toString());
page.url.searchParams.set("br_lat", bounds.getSouth().toString());
page.url.searchParams.set("br_lon", bounds.getWest().toString());
goto(`?${$page.url.searchParams.toString()}`);
goto(`?${page.url.searchParams.toString()}`);
}
function handleMapInit() {
if (
$page.url.searchParams.has("tl_lat") &&
$page.url.searchParams.has("tl_lon") &&
$page.url.searchParams.has("br_lat") &&
$page.url.searchParams.has("br_lon")
page.url.searchParams.has("tl_lat") &&
page.url.searchParams.has("tl_lon") &&
page.url.searchParams.has("br_lat") &&
page.url.searchParams.has("br_lon")
) {
const boundingBox: M.LngLatBoundsLike = [
[
parseFloat($page.url.searchParams.get("br_lon")!),
parseFloat($page.url.searchParams.get("tl_lat")!),
parseFloat(page.url.searchParams.get("br_lon")!),
parseFloat(page.url.searchParams.get("tl_lat")!),
],
[
parseFloat($page.url.searchParams.get("tl_lon")!),
parseFloat($page.url.searchParams.get("br_lat")!),
parseFloat(page.url.searchParams.get("tl_lon")!),
parseFloat(page.url.searchParams.get("br_lat")!),
],
];
map.fitBounds(boundingBox, { animate: false });
map?.fitBounds(boundingBox, { animate: false });
} else if (
$page.url.searchParams.has("lat") &&
$page.url.searchParams.has("lon")
page.url.searchParams.has("lat") &&
page.url.searchParams.has("lon")
) {
const lat = $page.url.searchParams.get("lat");
const lon = $page.url.searchParams.get("lon");
map.setZoom(14);
map.setCenter([parseFloat(lon!), parseFloat(lat!)]);
const lat = page.url.searchParams.get("lat");
const lon = page.url.searchParams.get("lon");
map?.setZoom(14);
map?.setCenter([parseFloat(lon!), parseFloat(lat!)]);
} else if (settings && settings.mapFocus == "trails") {
const boundingBox: M.LngLatBoundsLike = [
[maxBoundingBox.min_lon, maxBoundingBox.max_lat],
[maxBoundingBox.max_lon, maxBoundingBox.min_lat],
];
map.fitBounds(boundingBox, { animate: false, padding: 32 });
map?.fitBounds(boundingBox, { animate: false, padding: 32 });
} else if (
settings &&
settings.mapFocus == "location" &&
settings.location
) {
map.setZoom(12);
map.setCenter([settings.location.lon, settings.location.lat]);
map?.setZoom(12);
map?.setCenter([settings.location.lon, settings.location.lat]);
} else {
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
map.setZoom(12);
map.setCenter([lon, lat]);
map?.setZoom(12);
map?.setCenter([lon, lat]);
},
(error) => {
console.error("Error getting user location:", error);
@@ -212,19 +218,21 @@
<div class="flex items-center gap-2 md:gap-4">
<Search
extraClasses="w-full"
on:update={(e) => search(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
onupdate={search}
onclick={handleSearchClick}
placeholder="{$_('search-for-trails-places')}..."
items={searchDropdownItems}
></Search>
<button
aria-label="Open filter"
class="btn-icon"
on:click={() => (showFilter = !showFilter)}
onclick={() => (showFilter = !showFilter)}
><i class="fa fa-sliders"></i></button
>
<button
aria-label="Toggle map"
class="btn-icon md:hidden"
on:click={() => (showMap = !showMap)}
onclick={() => (showMap = !showMap)}
><i
class="fa-regular fa-{showMap
? 'rectangle-list'
@@ -238,8 +246,8 @@
categories={$categories}
showTrailSearch={false}
showCitySearch={false}
{filter}
on:update={(e) => handleFilterUpdate(e.detail)}
bind:filter
onupdate={handleFilterUpdate}
></TrailFilterPanel>
</div>
{/if}
@@ -255,14 +263,12 @@
<EmptyStateSearch></EmptyStateSearch>
{/if}
{#each trails as trail, i}
<a
href="map/trail/{trail.id}"
>
<a href="map/trail/{trail.id}">
<TrailCard
{trail}
on:mouseenter={() =>
onmouseenter={() =>
handleTrailCardMouseEnter(trail)}
on:mouseleave={() =>
onmouseleave={() =>
handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
@@ -275,8 +281,8 @@
class:hidden={!showMap && browser && window.innerWidth < 768}
>
<MapWithElevationMaplibre
on:moveend={handleMapMove}
on:init={handleMapInit}
onmoveend={handleMapMove}
oninit={handleMapInit}
{trails}
showElevation={false}
showInfoPopup={true}

View File

@@ -6,7 +6,7 @@
import "photoswipe/style.css";
import { _ } from "svelte-i18n";
let markers: M.Marker[];
let markers: M.Marker[] = $state([]);
</script>
<svelte:head>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import "$lib/assets/fonts/IBMPlexSans-Regular-normal";
import "$lib/assets/fonts/IBMPlexSans-SemiBold-bold";
import "$lib/assets/fonts/fa-solid-900-normal";
@@ -15,8 +15,11 @@
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { calculatePixelPerMeter, calculateScaleFactor } from "$lib/util/maplibre_util";
import {
calculatePixelPerMeter,
calculateScaleFactor,
} from "$lib/util/maplibre_util";
import { createRect, createText } from "$lib/util/svg_util";
import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index";
import { jsPDF } from "jspdf";
@@ -24,10 +27,10 @@
import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n";
let map: M.Map;
let showGrid: boolean = true;
let map: M.Map | undefined = $state();
let showGrid: boolean = $state(true);
const settings: Settings = $page.data.settings;
const settings: Settings = page.data.settings;
const paperSizes: { text: string; value: keyof typeof paperDimensions }[] =
[
@@ -56,29 +59,31 @@
},
},
};
let selectedPaperSize = paperSizes[0].value;
let selectedPaperSize = $state(paperSizes[0].value);
const orientations: { text: string; value: "portrait" | "landscape" }[] = [
{ text: "Portrait", value: "portrait" },
{ text: "Landscape", value: "landscape" },
];
let selectedOrientation: "portrait" | "landscape" = orientations[0].value;
let selectedOrientation: "portrait" | "landscape" = $state(
orientations[0].value,
);
const gridOptions = [
{ text: $_("degrees"), value: "degree" },
{ text: $_("no-grid"), value: "off" },
];
let selectedGrid = gridOptions[0].value;
let selectedGrid = $state(gridOptions[0].value);
let scale: number = 1;
let scale: number = $state(1);
let printLoading: boolean = false;
let printLoading: boolean = $state(false);
let includeDescription: boolean = false;
let includeDescription: boolean = $state(false);
onMount(() => {
let qrcode = new QrCodeWithLogo({
content: $page.url.href.replace("/print", ""),
content: page.url.href.replace("/print", ""),
image: document.getElementById("qrcode") as HTMLImageElement,
logo: {
src: "/favicon.png",
@@ -87,6 +92,9 @@
});
async function print() {
if (!map) {
return;
}
printLoading = true;
const doc = new jsPDF(selectedOrientation, "mm", [
paperDimensions[selectedPaperSize].pdf[
@@ -110,7 +118,7 @@
try {
var img = document.createElement("img");
var dimensions = map.getCanvas().getBoundingClientRect();
let dimensions = map.getCanvas().getBoundingClientRect();
img.width = dimensions.width;
img.height = dimensions.height;
let ratio = img.height / img.width;
@@ -289,7 +297,7 @@
const elevationChartCanvas = document.getElementById(
"elevation-profile-chart",
) as HTMLCanvasElement;
var dimensions = elevationChartCanvas.getBoundingClientRect();
dimensions = elevationChartCanvas.getBoundingClientRect();
img.width = dimensions.width;
img.height = dimensions.height;
ratio = img.height / img.width;
@@ -469,25 +477,25 @@
bind:value={selectedPaperSize}
items={paperSizes}
label={$_("paper-size")}
on:change={updateMapSize}
onchange={updateMapSize}
></Select>
<Select
bind:value={selectedOrientation}
items={orientations}
label={$_("orientation")}
on:change={updateMapSize}
onchange={updateMapSize}
></Select>
<Select
bind:value={selectedGrid}
items={gridOptions}
label={$_("grid")}
on:change={toggleGrid}
onchange={toggleGrid}
></Select>
<div>
<input
id="description-checkbox"
type="checkbox"
bind:value={includeDescription}
bind:checked={includeDescription}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
/>
<label for="description-checkbox" class="ms-2 text-sm"
@@ -499,7 +507,7 @@
loading={printLoading}
extraClasses="mt-2"
primary={true}
on:click={print}>{$_("print")}!</Button
onclick={print}>{$_("print")}!</Button
>
</div>
@@ -518,7 +526,7 @@
trails={[$trail]}
waypoints={$trail.expand?.waypoints}
activeTrail={0}
on:zoom={(e) => updateScale(e.detail)}
onzoom={updateScale}
bind:map
{showGrid}
elevationProfileContainer="elevation-profile"

View File

@@ -1,41 +1,42 @@
<script lang="ts">
import { invalidateAll } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import Button from "$lib/components/base/button.svelte";
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
import type { SelectItem } from "$lib/components/base/select.svelte";
import ProfileShareModal from "$lib/components/profile/profile_share_modal.svelte";
import TrailShareModal from "$lib/components/trail/trail_share_modal.svelte";
import { follows_create, follows_delete } from "$lib/stores/follow_store";
import { currentUser } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n";
export let data;
let { data, children } = $props();
let openShareModal: () => void;
let profileShareModal: ProfileShareModal;
let followLoading: boolean = false;
let followLoading: boolean = $state(false);
const profileLinks: DropdownItem[] = [
{
text: $_("profile"),
value: `/profile/${$page.params.id}`,
value: `/profile/${page.params.id}`,
icon: "user",
},
{
text: $_("trail", { values: { n: 2 } }),
value: `/profile/${$page.params.id}/trails`,
value: `/profile/${page.params.id}/trails`,
icon: "route",
},
{
text: $_("statistics"),
value: `/profile/${$page.params.id}/stats`,
value: `/profile/${page.params.id}/stats`,
icon: "chart-pie",
},
];
$: activeIndex = profileLinks.findIndex(
(l) => l.value === $page.url.pathname,
let activeIndex = $derived(
profileLinks.findIndex((l) => l.value === page.url.pathname),
);
async function follow() {
@@ -90,14 +91,14 @@
{/if}
<div class="flex gap-x-6 text-sm px-6">
<a
class:font-bold={$page.url.pathname.endsWith("followers")}
class:font-bold={page.url.pathname.endsWith("followers")}
href="/profile/{data.user.id}/users/followers"
>
<p class="font-semibold">{data.followers}</p>
<p>{$_("followers")}</p>
</a>
<a
class:font-bold={$page.url.pathname.endsWith("following")}
class:font-bold={page.url.pathname.endsWith("following")}
href="/profile/{data.user.id}/users/following"
>
<p class="font-semibold">{data.following}</p>
@@ -112,7 +113,7 @@
primary={!data.follow}
secondary={!!data.follow}
icon={data.follow ? "check" : ""}
on:click={() => follow()}
onclick={() => follow()}
>
{data.follow ? $_("following") : $_("follow")}</Button
>
@@ -132,7 +133,7 @@
<div class="px-6 mb-4 flex flex-col gap-2">
<button
class="btn-secondary basis-full"
on:click={() => openShareModal()}
onclick={() => profileShareModal.openModal()}
>{$_("share-profile")}</button
>
<a
@@ -140,11 +141,11 @@
href="/settings/profile">{$_("settings")}</a
>
</div>
<ProfileShareModal bind:openModal={openShareModal}
<ProfileShareModal bind:this={profileShareModal}
></ProfileShareModal>
{/if}
</div>
<div class="md:mr-6">
<slot></slot>
{@render children?.()}
</div>
</div>

View File

@@ -1,20 +1,23 @@
<script lang="ts">
import ActivityCard from "$lib/components/profile/activity_card.svelte";
import { activities_index } from "$lib/stores/activity_store.js";
import { getFileURL } from "$lib/util/file_util.js";
import { _ } from "svelte-i18n";
import { goto } from "$app/navigation";
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import ActivityCard from "$lib/components/profile/activity_card.svelte";
import { activities_index } from "$lib/stores/activity_store.js";
import { theme } from "$lib/stores/theme_store.js";
import { getFileURL } from "$lib/util/file_util.js";
import { _ } from "svelte-i18n";
export let data;
let { data } = $props();
let activities = $state(data.activities);
let loading: boolean = false;
$: pagination = {
let pagination = $derived({
page: data.activities.page,
totalPages: data.activities.totalPages,
};
});
async function onListScroll(e: Event) {
if (
@@ -31,11 +34,11 @@
async function loadNextPage() {
pagination.page += 1;
data.activities = await activities_index(data.user.id, pagination.page);
activities = await activities_index(data.user.id, pagination.page);
}
</script>
<svelte:window on:scroll={onListScroll} />
<svelte:window onscroll={onListScroll} />
<svelte:head>
<title>{$_("profile")} | wanderer</title>
@@ -47,7 +50,7 @@
{$_("about")}
{data.user.username}
{#if data.isOwnProfile && data.settings.bio?.length}
<a class="ml-4" href="/settings/profile"
<a aria-label="Edit bio" class="ml-4" href="/settings/profile"
><i class="fa fa-pen text-base"></i></a
>
{/if}
@@ -109,26 +112,31 @@
</div>
<div class="space-y-4">
<h4 class="text-xl font-semibold">Timeline</h4>
{#if !data.activities.items.length && data.isOwnProfile}
{#if !activities.items.length && data.isOwnProfile}
<a class="btn-primary inline-block" href="/trails/edit/new"
>+ {$_("new-trail")}</a
>
{:else if !data.activities.items.length}
{:else if !activities.items.length}
<p class="w-full text-center text-gray-500 text-sm">
{$_("empty-activities", {
values: { username: data.user.username },
})}
</p>
{/if}
{#each data.activities.items as activity}
<a
class="block"
href={activity.type == "trail"
? `/trail/view/${activity.id}`
: `/trail/view/${activity.trail_id}?t=3`}
{#each activities.items as activity}
<div
role="presentation"
class="cursor-pointer"
onclick={() => {
goto(
activity.type == "trail"
? `/trail/view/${activity.id}`
: `/trail/view/${activity.trail_id}?t=3`,
);
}}
>
<ActivityCard {activity} user={data.user}></ActivityCard>
</a>
</div>
{/each}
</div>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import Calendar from "$lib/components/base/calendar.svelte";
import Datepicker from "$lib/components/base/datepicker.svelte";
import MultiSelect from "$lib/components/base/multi_select.svelte";
@@ -18,6 +18,8 @@
formatSpeed,
formatTimeHHMM,
} from "$lib/util/format_util";
import Bar from "$lib/vendor/svelte-chartjs/bar.svelte";
import Pie from "$lib/vendor/svelte-chartjs/pie.svelte";
import {
ArcElement,
BarElement,
@@ -28,10 +30,9 @@
Title,
Tooltip,
} from "chart.js";
import { Bar, Pie } from "svelte-chartjs";
import { _ } from "svelte-i18n";
export let data;
let { data } = $props();
ChartJS.register(
Title,
@@ -43,7 +44,7 @@
BarElement,
);
const filter = data.filter;
const filter = $state(data.filter);
const categorySelectItems: SelectItem[] = $categories.map((c) => ({
value: c.id,
@@ -69,7 +70,7 @@
},
];
let barChartSelectedValue = barChartSelectItems[0].value;
let barChartSelectedValue = $state(barChartSelectItems[0].value);
const categoryColors = [
"#fb8500",
@@ -86,7 +87,7 @@
elevation_loss: 3.28084,
};
$: logCategories = $summitLogs.reduce(
let logCategories = $derived($summitLogs.reduce(
(acc, log) => {
const cat =
log.expand?.trails_via_summit_logs?.at(0)?.expand?.category
@@ -95,18 +96,18 @@
return acc;
},
{} as Record<string, number>,
);
));
$: categoryLabels = Object.keys(logCategories).sort();
$: categoryValues = Object.values(logCategories);
$: categoryColorMap = Object.fromEntries(
let categoryLabels = $derived(Object.keys(logCategories).sort());
let categoryValues = $derived(Object.values(logCategories));
let categoryColorMap = $derived(Object.fromEntries(
categoryLabels.map((label, index) => [
label,
categoryColors[index % categoryColors.length],
]),
);
));
$: categoryChartData = {
let categoryChartData = $derived({
labels: categoryLabels,
datasets: [
{
@@ -114,10 +115,10 @@
backgroundColor: categoryColors,
},
],
};
});
function barChartUnit() {
const unit = $page.data.settings?.unit ?? "metric";
const unit = page.data.settings?.unit ?? "metric";
switch (barChartSelectedValue) {
case "duration":
@@ -130,7 +131,7 @@
}
}
$: barChartDataByDate = $summitLogs.reduce(
let barChartDataByDate = $derived($summitLogs.reduce(
(acc, log) => {
const date = new Date(log.date).toLocaleDateString(undefined, {
month: "2-digit",
@@ -145,7 +146,7 @@
} else if (barChartSelectedValue === "duration") {
acc[date] = acc[date] / 60 / 60;
}
if ($page.data.settings?.unit !== "metric") {
if (page.data.settings?.unit !== "metric") {
acc[date] =
acc[date] *
(conversionFactors as any)[barChartSelectedValue];
@@ -153,12 +154,12 @@
return acc;
},
{} as Record<string, number>,
);
));
$: barChartLabels = Object.keys(barChartDataByDate); // Dates as labels
$: barChartValues = Object.values(barChartDataByDate);
let barChartLabels = $derived(Object.keys(barChartDataByDate)); // Dates as labels
let barChartValues = $derived(Object.values(barChartDataByDate));
$: barChartData = {
let barChartData = $derived({
labels: barChartLabels,
datasets: [
{
@@ -170,36 +171,36 @@
borderRadius: 15,
},
],
};
});
$: totalDistance = $summitLogs.reduce(
let totalDistance = $derived($summitLogs.reduce(
(sum, log) => sum + (log.distance ?? 0),
0,
);
));
$: totalDuration = $summitLogs.reduce(
let totalDuration = $derived($summitLogs.reduce(
(sum, log) => sum + (log.duration ?? 0),
0,
);
));
$: totalElevationGain = $summitLogs.reduce(
let totalElevationGain = $derived($summitLogs.reduce(
(sum, log) => sum + (log.elevation_gain ?? 0),
0,
);
));
$: totalElevationLoss = $summitLogs.reduce(
let totalElevationLoss = $derived($summitLogs.reduce(
(sum, log) => sum + (log.elevation_loss ?? 0),
0,
);
));
$: averageSpeed =
totalDuration > 0
let averageSpeed =
$derived(totalDuration > 0
? $summitLogs.reduce(
(sum, log) =>
sum + (log.distance && log.duration ? log.distance : 0),
0,
) / totalDuration
: undefined;
: undefined);
function updateFilterCategory(categories: SelectItem[]) {
filter.category = categories.map((c) => c.value);
@@ -216,7 +217,7 @@
}
async function loadSummitLogs() {
const logs = await summit_logs_index($page.params.id, filter);
const logs = await summit_logs_index(page.params.id, filter);
summitLogs.set(logs.items);
}
</script>
@@ -230,18 +231,18 @@
class="flex flex-wrap lg:flex-nowrap col-start-1 lg:col-start-2 gap-x-4 justify-end"
>
<MultiSelect
on:change={(e) => updateFilterCategory(e.detail)}
onchange={updateFilterCategory}
label={$_("categories")}
items={categorySelectItems}
placeholder={`${$_("filter-categories")}...`}
></MultiSelect>
<Datepicker
on:change={loadSummitLogs}
onchange={loadSummitLogs}
bind:value={filter.startDate}
label={$_("after")}
></Datepicker>
<Datepicker
on:change={loadSummitLogs}
onchange={loadSummitLogs}
bind:value={filter.endDate}
label={$_("before")}
></Datepicker>
@@ -249,7 +250,7 @@
<div class="space-y-4 grow-0 lg:mr-4">
<div class="border border-input-border rounded-xl p-6">
<Calendar
on:click={(e) => handleDateClick(e.detail)}
onclick={handleDateClick}
logs={$summitLogs}
colorMap={categoryColorMap}
></Calendar>

View File

@@ -1,29 +1,35 @@
<script lang="ts">
import TrailList from "$lib/components/trail/trail_list.svelte";
import type { Trail, TrailFilter } from "$lib/models/trail.js";
import { trails_search_filter } from "$lib/stores/trail_store";
import { _ } from "svelte-i18n";
export let data;
let { data } = $props();
let loading = true;
let loading = $state(true);
$: pagination = {
let pagination = $state({
page: data.trails.page,
totalPages: data.trails.totalPages,
};
});
let trails = $state(data.trails);
let filter: TrailFilter = $state(data.filter);
async function handleFilterUpdate() {
loading = true;
data.trails = await trails_search_filter(data.filter, pagination.page);
trails = await trails_search_filter(filter, pagination.page);
loading = false;
}
async function paginate(page: number) {
pagination.page = page;
data.trails = await trails_search_filter(data.filter, page);
trails = await trails_search_filter(filter, page);
}
</script>
<svelte:head>
<title>{$_("profile")} | wanderer</title>
</svelte:head>
@@ -31,8 +37,8 @@
{pagination}
{loading}
fullWidthCards={true}
trails={data.trails.items}
filter={data.filter}
on:update={() => handleFilterUpdate()}
on:pagination={(e) => paginate(e.detail)}
trails={trails.items}
{filter}
onupdate={handleFilterUpdate}
onpagination={paginate}
></TrailList>

View File

@@ -1,18 +1,24 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import { follows_index } from "$lib/stores/follow_store.js";
import { getFileURL } from "$lib/util/file_util.js";
import { _ } from "svelte-i18n";
export let data;
let { data } = $props();
let follows = $state(data.follows);
let loading: boolean = false;
$: key = ($page.params.type == "following" ? "followee" : "follower") as "follower" | "followee";
let key = $derived(
(page.params.type == "following" ? "followee" : "follower") as
| "follower"
| "followee",
);
$: pagination = {
let pagination = $derived({
page: data.follows.page,
totalPages: data.follows.totalPages,
};
});
async function onListScroll(e: Event) {
if (
@@ -29,18 +35,18 @@
async function loadNextPage() {
pagination.page += 1;
data.follows = await follows_index(
{ followee: $page.params.id },
follows = await follows_index(
{ followee: page.params.id },
pagination.page,
);
}
</script>
<svelte:window on:scroll={onListScroll} />
<svelte:window onscroll={onListScroll} />
<div>
<h1 class="text-3xl font-semibold mb-4">{$_($page.params.type)}</h1>
<h1 class="text-3xl font-semibold mb-4">{$_(page.params.type)}</h1>
<ul class="space-y-4">
{#each data.follows.items as follow}
{#each follows.items as follow}
{#if !follow.expand?.[key].private}
<a href="/profile/{follow.expand?.[key].id}">
<li

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto, invalidateAll } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import Button from "$lib/components/base/button.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
@@ -16,7 +16,7 @@
import { _ } from "svelte-i18n";
import { INVALID, z } from "zod";
let loading: boolean = false;
let loading: boolean = $state(false);
const { form, errors } = createForm<User>({
initialValues: {
id: "",
@@ -77,7 +77,7 @@
)
? (window.navigator.language as Language)
: Language.en;
await settings_update({ id: $page.data.settings.id, language });
await settings_update({ id: page.data.settings.id, language });
} catch (e) {
console.error(e);
} finally {

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>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { page } from "$app/stores";
import { PUBLIC_VALHALLA_URL } from "$env/static/public";
import Button from "$lib/components/base/button.svelte";
import Datepicker from "$lib/components/base/datepicker.svelte";
@@ -56,51 +55,47 @@
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
import { theme } from "$lib/stores/theme_store.js";
import {
createAnchorMarker,
createMarkerFromWaypoint,
} from "$lib/util/maplibre_util";
import { country_codes } from "$lib/util/country_code_util.js";
import { createAnchorMarker } from "$lib/util/maplibre_util";
import { validator } from "@felte/validator-zod";
import cryptoRandomString from "crypto-random-string";
import { createForm } from "felte";
import * as M from "maplibre-gl";
import { onMount } from "svelte";
import { onMount, untrack } from "svelte";
import { _ } from "svelte-i18n";
import { backInOut } from "svelte/easing";
import { scale } from "svelte/transition";
import { z } from "zod";
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
import { country_codes } from "$lib/util/country_code_util.js";
import { page } from "$app/state";
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
export let data;
let { data } = $props();
let map: M.Map;
let mapTrail: Trail[] = [];
$: gpxData = $formData.expand!?.gpx_data;
$: if (gpxData) {
updateTrailOnMap();
}
let map: M.Map | undefined = $state();
let mapTrail: Trail[] = $state([]);
let lists = $state(data.lists)
let openWaypointModal: () => void;
let openSummitLogModal: () => void;
let openListSelectModal: () => void;
let waypointModal: WaypointModal;
let summitLogModal: SummitLogModal;
let listSelectModal: ListSelectModal;
let loading = false;
let loading = $state(false);
let editingBasicInfo: boolean = false;
let editingBasicInfo: boolean = $state(false);
let photoFiles: File[] = [];
let photoFiles: File[] = $state([]);
let gpxFile: File | Blob | null = null;
let drawingActive = false;
let drawingActive = $state(false);
let overwriteGPX = false;
let draggingMarker = false;
let searchDropdownItems: SearchItem[] = [];
let searchDropdownItems: SearchItem[] = $state([]);
const ClientTrailCreateSchema = TrailCreateSchema.extend({
expand: z
@@ -121,9 +116,11 @@
{ text: $_("cycling"), value: "bicycle" },
{ text: $_("driving"), value: "auto" },
];
let selectedModeOfTransport = modesOfTransport[0].value;
let selectedModeOfTransport = $state(modesOfTransport[0].value);
let autoRouting = true;
let autoRouting = $state(true);
let listAddEnabled = $state(false);
const {
form,
@@ -135,10 +132,10 @@
...data.trail,
public: data.trail.id
? data.trail.public
: $page.data.settings?.privacy?.trails === "public",
: page.data.settings?.privacy?.trails === "public",
category:
data.trail.category ||
$page.data.settings?.category ||
page.data.settings?.category ||
$categories[0].id,
},
extend: validator({
@@ -158,33 +155,33 @@
(p) => !p.startsWith("data:image/svg+xml;base64"),
);
if ($formData.expand!.gpx_data && overwriteGPX) {
gpxFile = new Blob([$formData.expand!.gpx_data], {
if (form.expand!.gpx_data && overwriteGPX) {
gpxFile = new Blob([form.expand!.gpx_data], {
type: "text/xml",
});
}
if (
(!$formData.lat || !$formData.lon) &&
(!form.lat || !form.lon) &&
route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)
) {
$formData.lat = route.trk
form.lat = route.trk
?.at(0)
?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lat;
$formData.lon = route.trk
form.lon = route.trk
?.at(0)
?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lon;
}
if ($page.params.id === "new") {
if (page.params.id === "new") {
const createdTrail = await trails_create(
form as Trail,
photoFiles,
gpxFile,
);
$formData.id = createdTrail.id;
form.id = createdTrail.id;
} else {
await trails_update(
$trail,
@@ -194,6 +191,7 @@
);
}
listAddEnabled = true;
show_toast({
type: "success",
icon: "check",
@@ -269,11 +267,11 @@
$formData.expand!.gpx_data = gpxData;
setFields(
"category",
$page.data.settings.category || $categories[0].id,
page.data.settings.category || $categories[0].id,
);
setFields(
"public",
$page.data.settings?.privacy?.trails === "public",
page.data.settings?.privacy?.trails === "public",
);
const log = new SummitLog(parseResult.trail.date as string, {
@@ -382,21 +380,24 @@
function handleWaypointMenuClick(
currentWaypoint: Waypoint,
index: number,
e: CustomEvent<{ text: string; value: string }>,
item: DropdownItem,
) {
if (e.detail.value === "edit") {
if (item.value === "edit") {
waypoint.set(currentWaypoint);
openWaypointModal();
} else if (e.detail.value === "delete") {
waypointModal.openModal();
} else if (item.value === "delete") {
currentWaypoint.marker?.remove();
deleteWaypoint(index);
}
}
function beforeWaypointModalOpen() {
if (!map) {
return;
}
const mapCenter = map.getCenter();
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
openWaypointModal();
waypointModal.openModal();
}
function deleteWaypoint(index: number) {
@@ -442,24 +443,21 @@
function beforeSummitLogModalOpen() {
summitLog.set(new SummitLog(new Date().toISOString().split("T")[0]));
openSummitLogModal();
summitLogModal.openModal();
}
function saveSummitLog(e: CustomEvent<SummitLog>) {
const savedSummitLog = e.detail;
function saveSummitLog(log: SummitLog) {
let editedSummitLogIndex = $formData.expand!.summit_logs.findIndex(
(s) => s.id == savedSummitLog.id,
(s) => s.id == log.id,
);
if (editedSummitLogIndex >= 0) {
$formData.expand!.summit_logs[editedSummitLogIndex] =
savedSummitLog;
$formData.expand!.summit_logs[editedSummitLogIndex] = log;
} else {
savedSummitLog.id = cryptoRandomString({ length: 15 });
log.id = cryptoRandomString({ length: 15 });
$formData.expand!.summit_logs = [
...$formData.expand!.summit_logs,
savedSummitLog,
log,
];
}
}
@@ -467,12 +465,12 @@
function handleSummitLogMenuClick(
currentSummitLog: SummitLog,
index: number,
e: CustomEvent<{ text: string; value: string }>,
item: DropdownItem,
) {
if (e.detail.value === "edit") {
if (item.value === "edit") {
summitLog.set(currentSummitLog);
openSummitLogModal();
} else if (e.detail.value === "delete") {
summitLogModal.openModal();
} else if (item.value === "delete") {
$formData.expand!.summit_logs.splice(index, 1);
$formData.summit_logs.splice(index, 1);
$formData.expand!.summit_logs = $formData.expand!.summit_logs;
@@ -489,9 +487,9 @@
} else {
list = await lists_add_trail(list, $formData as Trail);
}
const index = data.lists.items.findIndex((l) => l.id == list.id);
const index = lists.items.findIndex((l) => l.id == list.id);
if (index >= 0) {
data.lists.items[index] = list;
lists.items[index] = list;
}
// await lists_index({ q: "", author: $currentUser?.id ?? "" }, 1, -1);
} catch (e) {
@@ -505,6 +503,9 @@
}
function startDrawing() {
if (!map) {
return;
}
drawingActive = true;
if (!route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.length) {
}
@@ -601,7 +602,7 @@
draggingMarker = false;
},
);
if (addtoMap) {
if (addtoMap && map) {
marker.addTo(map);
}
anchor.marker = marker;
@@ -797,7 +798,7 @@
}
function handleSearchClick(item: SearchItem) {
map.flyTo({
map?.flyTo({
center: [item.value._geo.lng, item.value._geo.lat],
zoom: 13,
animate: false,
@@ -819,12 +820,19 @@
icon: "city",
}));
}
let gpxData = $derived($formData.expand?.gpx_data);
$effect(() => {
if (gpxData) {
untrack(() => updateTrailOnMap());
}
});
</script>
<svelte:head>
<title
>{$page.params.id !== "new" ? `${$formData.name} | ${$_("edit")}` : $_("new-trail")} |
wanderer</title
>{page.params.id !== "new"
? `${$formData.name} | ${$_("edit")}`
: $_("new-trail")} | wanderer</title
>
</svelte:head>
@@ -835,8 +843,8 @@
use:form
>
<Search
on:update={(e) => searchCities(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
onupdate={(q) => searchCities(q)}
onclick={(item) => handleSearchClick(item)}
placeholder="{$_('search-cities')}..."
items={searchDropdownItems}
></Search>
@@ -846,7 +854,7 @@
primary={true}
type="button"
disabled={drawingActive}
on:click={openFileBrowser}
onclick={openFileBrowser}
>{$formData.expand?.gpx_data
? $_("upload-new-file")
: $_("upload-file")}</Button
@@ -860,7 +868,7 @@
<button
class="btn-primary"
type="button"
on:click={drawingActive ? stopDrawing : startDrawing}
onclick={drawingActive ? stopDrawing : startDrawing}
>
{$formData.expand?.gpx_data
? drawingActive
@@ -877,16 +885,17 @@
id="fileInput"
accept=".gpx,.GPX,.tcx,.TCX,.kml,.KML,.fit,.FIT"
style="display: none;"
on:change={handleFileSelection}
onchange={handleFileSelection}
/>
<hr class="border-separator" />
<div class="flex gap-x-2">
<h3 class="text-xl font-semibold">{$_("basic-info")}</h3>
<button
aria-label="Edit basic info"
type="button"
class="btn-icon"
style="font-size: 0.9rem"
on:click={() => (editingBasicInfo = !editingBasicInfo)}
onclick={() => (editingBasicInfo = !editingBasicInfo)}
><i class="fa fa-{editingBasicInfo ? 'check' : 'pen'}"
></i></button
>
@@ -1000,12 +1009,12 @@
</h3>
<ul>
{#each $formData.expand?.waypoints ?? [] as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
<li onmouseenter={() => openMarkerPopup(waypoint)}>
<WaypointCard
{waypoint}
mode="edit"
on:change={(e) =>
handleWaypointMenuClick(waypoint, i, e)}
onchange={(item) =>
handleWaypointMenuClick(waypoint, i, item)}
></WaypointCard>
</li>
{/each}
@@ -1013,7 +1022,7 @@
<button
class="btn-secondary"
type="button"
on:click={beforeWaypointModalOpen}
onclick={beforeWaypointModalOpen}
><i class="fa fa-plus mr-2"></i>{$_("add-waypoint")}</button
>
<hr class="border-separator" />
@@ -1033,7 +1042,8 @@
<SummitLogCard
{log}
mode="edit"
on:change={(e) => handleSummitLogMenuClick(log, i, e)}
onchange={(item) =>
handleSummitLogMenuClick(log, i, item)}
></SummitLogCard>
</li>
{/each}
@@ -1041,16 +1051,16 @@
<button
class="btn-secondary"
type="button"
on:click={beforeSummitLogModalOpen}
onclick={beforeSummitLogModalOpen}
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
>
{#if data.lists.items.length}
{#if lists.items.length}
<hr class="border-separator" />
<h3 class="text-xl font-semibold">
{$_("list", { values: { n: 2 } })}
</h3>
<div class="flex gap-4 flex-wrap">
{#each data.lists.items as list}
{#each lists.items as list}
{#if $formData.id && list.trails?.includes($formData.id)}
<div
class="flex gap-2 items-center border border-input-border rounded-xl p-2"
@@ -1073,9 +1083,9 @@
<Button
secondary={true}
tooltip={$_("save-your-trail-first")}
disabled={!$formData.id}
disabled={page.params.id == "new" && !listAddEnabled}
type="button"
on:click={openListSelectModal}
onclick={() => listSelectModal.openModal()}
><i class="fa fa-plus mr-2"></i>{$_("add-to-list")}</Button
>
{/if}
@@ -1110,25 +1120,22 @@
waypoints={$formData.expand?.waypoints}
drawing={drawingActive}
showTerrain={true}
onMarkerDragEnd={moveMarker}
onmarkerdragend={moveMarker}
activeTrail={0}
bind:map
on:click={(e) => handleMapClick(e.detail)}
on:segmentDragEnd={(e) => handleSegmentDragEnd(e.detail)}
onclick={(target) => handleMapClick(target)}
onsegmentdragend={(data) => handleSegmentDragEnd(data)}
></MapWithElevationMaplibre>
</div>
</div>
</main>
<WaypointModal
bind:openModal={openWaypointModal}
on:save={(e) => saveWaypoint(e.detail)}
></WaypointModal>
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
<WaypointModal bind:this={waypointModal} onsave={saveWaypoint}></WaypointModal>
<SummitLogModal bind:this={summitLogModal} onsave={(log) => saveSummitLog(log)}
></SummitLogModal>
<ListSelectModal
lists={data.lists.items}
bind:openModal={openListSelectModal}
on:change={(e) => handleListSelection(e.detail)}
lists={lists.items}
bind:this={listSelectModal}
onchange={(e) => handleListSelection(e)}
></ListSelectModal>
<style>

View File

@@ -1,18 +1,19 @@
<script>
<script lang="ts">
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import { trail } from "$lib/stores/trail_store";
import { _ } from "svelte-i18n";
import { page } from "$app/stores";
import { page } from "$app/state";
export let data;
let { data } = $props();
</script>
<TrailInfoPanel
activeTab={parseInt($page.url.searchParams.get("t") ?? 0)}
trail={data.trail}
mode="overview"
></TrailInfoPanel>
{#if data.trail}
<TrailInfoPanel
activeTab={parseInt(page.url.searchParams.get("t") ?? "0")}
trail={data.trail}
mode="overview"
></TrailInfoPanel>
{/if}
<svelte:head>
<title>{$trail.name} | {$_("trail", { values: { n: 1 } })} | wanderer</title
>

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 TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte";
import type { Trail, TrailFilter } from "$lib/models/trail";
@@ -8,16 +8,16 @@
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let filterExpanded: boolean = true;
let filterExpanded: boolean = $state(true);
let loading: boolean = true;
let loading: boolean = $state(true);
const filter: TrailFilter = $page.data.filter;
const pagination: { page: number; totalPages: number } = {
let filter: TrailFilter = $state(page.data.filter);
const pagination: { page: number; totalPages: number } = $state({
page: 1,
totalPages: 1,
};
let trails: Trail[] = [];
});
let trails: Trail[] = $state([]);
onMount(() => {
if (window.innerWidth < 768) {
@@ -34,12 +34,12 @@
loading = false;
}
async function paginate(page: number) {
pagination.page = page;
const response = await trails_search_filter(filter, page);
async function paginate(newPage: number) {
pagination.page = newPage;
const response = await trails_search_filter(filter, newPage);
trails = response.items;
$page.url.searchParams.set("page", page.toString());
goto(`?${$page.url.searchParams.toString()}`);
page.url.searchParams.set("page", newPage.toString());
goto(`?${page.url.searchParams.toString()}`);
}
</script>
@@ -51,17 +51,17 @@
class="grid grid-cols-1 md:grid-cols-[300px_1fr] items-start gap-8 max-w-7xl mx-6 md:mx-auto"
>
<TrailFilterPanel
categories={$page.data.categories}
{filter}
categories={page.data.categories}
bind:filter
{filterExpanded}
on:update={() => handleFilterUpdate()}
onupdate={handleFilterUpdate}
></TrailFilterPanel>
<TrailList
{filter}
bind:filter
{loading}
{trails}
{pagination}
on:update={() => handleFilterUpdate()}
on:pagination={(e) => paginate(e.detail)}
onupdate={handleFilterUpdate}
onpagination={paginate}
></TrailList>
</main>