Migrates to svelte 5
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import "photoswipe/style.css";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let markers: M.Marker[];
|
||||
let markers: M.Marker[] = $state([]);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user