adds docs

This commit is contained in:
Christian Beutel
2024-09-14 18:06:07 +02:00
parent ca34e6db95
commit 414fa9b752
29 changed files with 902 additions and 314 deletions

View File

@@ -1,3 +1,12 @@
# v0.9.0
## Features
- Complete visual overhaul of the list page
- Lists can now be shared with other users
## Bug fixes
- Fixes a bug that caused categories not to load properly on page reload
# v0.8.2
## Features
- The pagination is now available at top and bottom

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@@ -0,0 +1,391 @@
---
title: list-share
---
## GET show
GET /list-share/{id}
### Params
|Name|Location|Type|Required|Description|
|---|---|---|---|---|
|id|path|string| yes |none|
> Response Examples
> 200 Response
```json
{
"collectionId": "string",
"collectionName": "string",
"created": "2019-08-24T14:15:22Z",
"id": "string",
"permission": "view",
"list": "string",
"updated": "2019-08-24T14:15:22Z",
"user": "string"
}
```
### Responses
|HTTP Status Code |Meaning|Description|Data schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|none|Inline|
### Responses Data Schema
HTTP Status Code **200**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» collectionId|string|true|none||none|
|» collectionName|string|true|none||none|
|» created|string(date-time)|true|none||none|
|» id|string|true|none||none|
|» permission|string|true|none||none|
|» list|string|true|none||none|
|» updated|string(date-time)|true|none||none|
|» user|string|true|none||none|
#### Enum
|Name|Value|
|---|---|
|permission|view|
|permission|edit|
HTTP Status Code **404**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» url|string|true|none||none|
|» status|integer|true|none||none|
|» response|object|true|none||none|
|»» code|integer|true|none||none|
|»» message|string|true|none||none|
|»» data|object|true|none||none|
|» isAbort|boolean|true|none||none|
|» originalError|object|true|none||none|
|»» url|string|true|none||none|
|»» status|integer|true|none||none|
|»» data|object|true|none||none|
|»»» code|integer|true|none||none|
|»»» message|string|true|none||none|
|»»» data|object|true|none||none|
|» name|string|true|none||none|
## POST update
POST /list-share/{id}
> Body Parameters
```json
{
"list": "string",
"user": "string",
"permission": "view"
}
```
### Params
|Name|Location|Type|Required|Description|
|---|---|---|---|---|
|id|path|string| yes |none|
|Content-Type|header|string| yes |none|
|body|body|object| no |none|
|» list|body|string| yes |none|
|» user|body|string| yes |none|
|» permission|body|string| yes |none|
#### Enum
|Name|Value|
|---|---|
|» permission|view|
|» permission|edit|
> Response Examples
> 200 Response
```json
{
"collectionId": "string",
"collectionName": "string",
"created": "2019-08-24T14:15:22Z",
"id": "string",
"permission": "view",
"list": "string",
"updated": "2019-08-24T14:15:22Z",
"user": "string"
}
```
### Responses
|HTTP Status Code |Meaning|Description|Data schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|none|Inline|
### Responses Data Schema
HTTP Status Code **200**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» collectionId|string|true|none||none|
|» collectionName|string|true|none||none|
|» created|string(date-time)|true|none||none|
|» id|string|true|none||none|
|» permission|string|true|none||none|
|» list|string|true|none||none|
|» updated|string(date-time)|true|none||none|
|» user|string|true|none||none|
#### Enum
|Name|Value|
|---|---|
|permission|view|
|permission|edit|
HTTP Status Code **404**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» url|string|true|none||none|
|» status|integer|true|none||none|
|» response|object|true|none||none|
|»» code|integer|true|none||none|
|»» message|string|true|none||none|
|»» data|object|true|none||none|
|» isAbort|boolean|true|none||none|
|» originalError|object|true|none||none|
|»» url|string|true|none||none|
|»» status|integer|true|none||none|
|»» data|object|true|none||none|
|»»» code|integer|true|none||none|
|»»» message|string|true|none||none|
|»»» data|object|true|none||none|
|» name|string|true|none||none|
## GET list
GET /list-share
> Response Examples
> 200 Response
```json
[
{
"collectionId": "string",
"collectionName": "string",
"created": "string",
"id": "string",
"list": "string",
"permission": "string",
"updated": "string",
"user": "string",
"expand": {
"user": {
"avatar": "string",
"collectionId": "string",
"collectionName": "string",
"id": "string",
"username": "string"
}
}
}
]
```
### Responses
|HTTP Status Code |Meaning|Description|Data schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
### Responses Data Schema
HTTP Status Code **200**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» collectionId|string|false|none||none|
|» collectionName|string|false|none||none|
|» created|string|false|none||none|
|» id|string|false|none||none|
|» list|string|false|none||none|
|» permission|string|false|none||none|
|» updated|string|false|none||none|
|» user|string|false|none||none|
|» expand|object|false|none||none|
|»» user|object|true|none||none|
|»»» avatar|string|true|none||none|
|»»» collectionId|string|true|none||none|
|»»» collectionName|string|true|none||none|
|»»» id|string|true|none||none|
|»»» username|string|true|none||none|
## DELETE delete
DELETE /list-share/{id}
### Params
|Name|Location|Type|Required|Description|
|---|---|---|---|---|
|id|path|string| yes |none|
> Response Examples
> 200 Response
```json
{
"acknowledged": true
}
```
### Responses
|HTTP Status Code |Meaning|Description|Data schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|none|Inline|
### Responses Data Schema
HTTP Status Code **200**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» acknowledged|boolean|true|none||none|
HTTP Status Code **404**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» url|string|true|none||none|
|» status|integer|true|none||none|
|» response|object|true|none||none|
|»» code|integer|true|none||none|
|»» message|string|true|none||none|
|»» data|object|true|none||none|
|» isAbort|boolean|true|none||none|
|» originalError|object|true|none||none|
|»» url|string|true|none||none|
|»» status|integer|true|none||none|
|»» data|object|true|none||none|
|»»» code|integer|true|none||none|
|»»» message|string|true|none||none|
|»»» data|object|true|none||none|
|» name|string|true|none||none|
## PUT create
PUT /list-share
> Body Parameters
```json
{
"list": "string",
"user": "string",
"permission": "view"
}
```
### Params
|Name|Location|Type|Required|Description|
|---|---|---|---|---|
|body|body|object| no |none|
|» list|body|string| yes |none|
|» user|body|string| yes |none|
|» permission|body|string| yes |none|
#### Enum
|Name|Value|
|---|---|
|» permission|view|
|» permission|edit|
> Response Examples
> 200 Response
```json
{
"collectionId": "string",
"collectionName": "string",
"created": "2019-08-24T14:15:22Z",
"id": "string",
"permission": "view",
"list": "string",
"updated": "2019-08-24T14:15:22Z",
"user": "string"
}
```
### Responses
|HTTP Status Code |Meaning|Description|Data schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|none|Inline|
|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|none|Inline|
### Responses Data Schema
HTTP Status Code **200**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» collectionId|string|true|none||none|
|» collectionName|string|true|none||none|
|» created|string(date-time)|true|none||none|
|» id|string|true|none||none|
|» permission|string|true|none||none|
|» list|string|true|none||none|
|» updated|string(date-time)|true|none||none|
|» user|string|true|none||none|
#### Enum
|Name|Value|
|---|---|
|permission|view|
|permission|edit|
HTTP Status Code **400**
|Name|Type|Required|Restrictions|Title|description|
|---|---|---|---|---|---|
|» url|string|true|none||none|
|» status|integer|true|none||none|
|» response|object|true|none||none|
|»» code|integer|true|none||none|
|»» message|string|true|none||none|
|»» data|object|true|none||none|
|» isAbort|boolean|true|none||none|
|» originalError|object|true|none||none|
|»» url|string|true|none||none|
|»» status|integer|true|none||none|
|»» data|object|true|none||none|
|»»» code|integer|true|none||none|
|»»» message|string|true|none||none|
|»»» data|object|true|none||none|
|» name|string|true|none||none|
# Data Schema

View File

@@ -3,18 +3,27 @@ title: Lists
description: What are lists and how can we use them?
---
Lists allow you to further organise your trails by bundling them in one place. Over at `/lists` you can create, update, and delete your custom lists. You can view all trails in a list either on a map or in a simple list.
Lists allow you to further organise your trails by bundling them in one place. Over at `/lists` you can create, update, share and delete your custom lists.
![Lists](../../../assets/guides/wanderer_lists.gif)
## Create a list
Simply click the `+ Create new list` button and give your list a name. Optionally, you can also add an avatar and a short description.
Simply click the <button class="h-10 text-white rounded-lg px-4 py-2 mx-2 bg-primary font-semibold transition-all hover:bg-primary-hover focus:ring-4 ring-zinc-400 leading-none">+ New List</button> button. You will be redirected to the editor where you can give your list a name and add one or more trails to it. Optionally, you can also add an avatar and a short description.
## Add a trail
To add a trail to the list simply search for it and click on the search suggestion. After adding a trail you can change its position in the list using the up- and down arrows.
To add a trail to a list head over to `/trails` and click on the trail you want to add. From the ⋮ menu select "Add to list" and select the list you want to add the trail to. To remove the trail simply repeat this process.
Once you are done creating your list simply click the <button class="h-10 text-white rounded-lg px-4 py-2 mx-2 bg-primary font-semibold transition-all hover:bg-primary-hover focus:ring-4 ring-zinc-400 leading-none">Save List</button> button at the bottom.
![Lists edit](../../../assets/guides/wanderer_lists_edit.png)
## Share a list
You can also share a list to allow other users access it. To do so select the list in the overview and click "Share" in the ⋮ menu. Everything else works analogous to <a href="/guides/share-trails#share-a-trail">sharing a trail</a>. Depending on the permission you set for the shared list a user can simply view the list or also modify its contents.
:::caution
Sharing a list with another user will also automatically share all trails contained in that list with the user. Unsharing a list will _not_ automatically unshare all trails contained in the list.
:::
## Edit & delete a list
To edit or delete a list simply select the respective entry from the ⋮ menu.
To edit or delete a list first select it from the overview. Then you can select the respective entry from the ⋮ menu.

View File

@@ -6,7 +6,7 @@
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
export let title: string = "Confirm Deletion";
export let title: string = $_('confirm-deletion');
export let text: string;
export let action: string = "delete";

View File

@@ -6,43 +6,28 @@
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import { _ } from "svelte-i18n";
import ShareInfo from "../share_info.svelte";
import { currentUser } from "$lib/stores/user_store";
export let list: List;
export let active: boolean = false;
$: cumulativeDistance = list.expand?.trails.reduce(
$: cumulativeDistance = list.expand?.trails?.reduce(
(s, b) => s + b.distance!,
0,
);
$: cumulativeElevationGain = list.expand?.trails.reduce(
$: cumulativeElevationGain = list.expand?.trails?.reduce(
(s, b) => s + b.elevation_gain!,
0,
);
$: cumulativeDuration = list.expand?.trails.reduce(
$: cumulativeDuration = list.expand?.trails?.reduce(
(s, b) => s + b.duration!,
0,
);
$: listIsShared = (list.expand?.list_share_via_list?.length ?? 0) > 0;
$: allowEdit =
list.author == $currentUser?.id ||
list.expand?.list_share_via_list?.some((s) => s.permission == "edit");
$: dropdownItems = [
...(list.author == $currentUser?.id
? [{ text: $_("share"), value: "share" }]
: []),
...(allowEdit ? [{ text: $_("edit"), value: "edit" }] : []),
...(list.author == $currentUser?.id
? [{ text: $_("delete"), value: "delete" }]
: []),
];
</script>
<div
@@ -72,9 +57,6 @@
{#if listIsShared}
<ShareInfo type="list" subject={list}></ShareInfo>
{/if}
{#if dropdownItems.length}
<Dropdown items={dropdownItems} on:change></Dropdown>
{/if}
</div>
<div class="flex mt-1 gap-x-4 text-sm text-gray-500 whitespace-nowrap flex-wrap">
<span
@@ -94,9 +76,9 @@
>
</div>
<p class="text-sm text-gray-500 mb-2">
{list.expand?.trails.length ?? 0}
{list.expand?.trails?.length ?? 0}
{$_("trail", {
values: { n: list.expand?.trails.length ?? 0 },
values: { n: list.expand?.trails?.length ?? 0 },
})}
</p>
<p

View File

@@ -1,113 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { browser } from "$app/environment";
import { listSchema, type List } from "$lib/models/list";
import { list } from "$lib/stores/list_store";
import { createForm } from "$lib/vendor/svelte-form-lib/index";
import { util } from "$lib/vendor/svelte-form-lib/util";
import { _ } from "svelte-i18n";
import Modal from "../base/modal.svelte";
import TextField from "../base/text_field.svelte";
import Textarea from "../base/textarea.svelte";
import { getFileURL } from "$lib/util/file_util";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
const dispatch = createEventDispatcher();
let previewURL = "";
const { form, errors, handleChange, handleSubmit } = createForm<List>({
initialValues: $list,
validationSchema: listSchema,
onSubmit: async (submittedList) => {
dispatch("save", { list: submittedList, avatar: (document.getElementById("avatar") as HTMLInputElement).files![0] });
(document.getElementById("avatar") as HTMLInputElement).value = "";
closeModal!();
},
});
function openAvatarBrowser() {
document.getElementById("avatar")!.click();
}
function handleAvatarSelection() {
const files = (document.getElementById("avatar") as HTMLInputElement)
.files;
if (!files) {
return;
}
previewURL = URL.createObjectURL(files[0]);
}
$: if (browser) {
form.set(util.cloneDeep($list));
previewURL = getFileURL($list, $list.avatar) ?? "";
}
</script>
<Modal
id="list-modal"
title={$form.id ? $_("edit-list") : $_("new-list")}
let:openModal
bind:openModal
bind:closeModal
>
<slot {openModal} />
<form
id="list-form"
slot="content"
class="modal-content space-y-4"
on:submit={handleSubmit}
>
<label for="avatar" class="text-sm font-medium block"> {$_('avatar')} </label>
<input
name="avatar"
type="file"
id="avatar"
accept="image/*"
style="display: none;"
on:change={handleAvatarSelection}
/>
<div class="flex items-center gap-4">
{#if previewURL.length > 0}
<img
class="w-32 aspect-square rounded-full object-cover"
alt="avatar"
src={previewURL}
/>
{/if}
<button
class="btn-secondary"
type="button"
on:click={openAvatarBrowser}>{$_('change')}...</button
>
</div>
<TextField
name="name"
label={$_("name")}
bind:value={$form.name}
error={$errors.name}
on:change={handleChange}
></TextField>
<Textarea
name="description"
label={$_("description")}
bind:value={$form.description}
error={$errors.description}
on:change={handleChange}
></Textarea>
</form>
<div slot="footer" class="flex items-center gap-4">
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-primary" type="submit" form="list-form" name="save"
>{$_("save")}</button
>
</div>
</Modal>

View File

@@ -0,0 +1,156 @@
<script lang="ts">
import type { List } from "$lib/models/list";
import { getFileURL } from "$lib/util/file_util";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import TrailListItem from "../trail/trail_list_item.svelte";
import type { Trail } from "$lib/models/trail";
import { createEventDispatcher } from "svelte";
import { currentUser } from "$lib/stores/user_store";
import Dropdown from "../base/dropdown.svelte";
import ShareInfo from "../share_info.svelte";
export let list: List;
const dispatch = createEventDispatcher();
$: cumulativeDistance = list.expand?.trails?.reduce(
(s, b) => s + b.distance!,
0,
);
$: cumulativeElevationGain = list.expand?.trails?.reduce(
(s, b) => s + b.elevation_gain!,
0,
);
$: cumulativeDuration = list.expand?.trails?.reduce(
(s, b) => s + b.duration!,
0,
);
$: allowEdit =
list.author == $currentUser?.id ||
list.expand?.list_share_via_list?.some((s) => s.permission == "edit");
$: dropdownItems = [
...(list.author == $currentUser?.id
? [{ text: $_("share"), value: "share", icon: "share" }]
: []),
...(allowEdit
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
: []),
...(list.author == $currentUser?.id
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
: []),
];
$: listIsShared = (list.expand?.list_share_via_list?.length ?? 0) > 0;
let fullDescription: boolean = false;
function handleTrailSelect(trail: Trail, index: number) {
dispatch("click", { trail, index });
}
function handleTrailMouseEnter(trail: Trail, index: number) {
dispatch("mouseenter", { trail, index });
}
function handleTrailMouseLeave(trail: Trail, index: number) {
dispatch("mouseleave", { trail, index });
}
</script>
<div class="relative">
{#if listIsShared}
<div class="absolute top-8 right-8 bg-white rounded-full w-8 py-1 text-center">
<ShareInfo type="list" subject={list}></ShareInfo>
</div>
{/if}
{#if dropdownItems.length}
<div class="absolute bottom-8 right-8">
<Dropdown
items={dropdownItems}
on:change
let:toggleMenu={openDropdown}
><button
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
on:click={openDropdown}
>
<i class="fa fa-ellipsis-vertical"></i>
</button></Dropdown
>
</div>
{/if}
{#if list.avatar}
<img
class="w-full object-cover"
src={getFileURL(list, list.avatar)}
alt="avatar"
/>
{:else}
<div class="flex w-full shrink-0 items-center justify-center min-h-72">
<i class="fa fa-table-list text-5xl"></i>
</div>
{/if}
</div>
<div class="p-4 md:p-6">
<h4 class="text-2xl font-semibold mb-4">{list.name}</h4>
<hr />
<div class="flex my-4 gap-x-4 font-semibold whitespace-nowrap flex-wrap justify-around">
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
cumulativeDistance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
cumulativeElevationGain,
)}</span
>
<span
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
cumulativeDuration,
)}</span
>
</div>
<hr class="mb-4" />
<p
class="text-gray-500 whitespace-pre-wrap {fullDescription
? ''
: 'max-h-24 overflow-hidden text-ellipsis'}"
>
{!fullDescription
? list.description?.substring(0, 100)
: list.description}
{#if (list.description?.length ?? 0) > 100 && !fullDescription}
<button on:click={() => (fullDescription = true)}>
... <span class="underline">{$_("read-more")}</span></button
>
{/if}
</p>
<h5 class="text-xl font-semibold my-4">
{list.trails?.length ?? 0}
{$_("trail", { values: { n: list.trails?.length ?? 0 } })}
</h5>
<div class="space-y-2">
{#each list.expand?.trails ?? [] as trail, i}
<div
role="presentation"
on:click={() => handleTrailSelect(trail, i)}
on:mouseenter={() => handleTrailMouseEnter(trail, i)}
on:mouseleave={() => handleTrailMouseLeave(trail, i)}
>
<TrailListItem {trail} showDescription={false}></TrailListItem>
</div>
{/each}
</div>
</div>

View File

@@ -20,6 +20,7 @@
export let map: any | null = null;
export let options: any = {};
export let activeTrailIndex: number | null = 0;
export let markers: any[] = [];
const dispatch = createEventDispatcher();
@@ -28,7 +29,9 @@
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
$: gpxData = trails.map((t) => t.expand.gpx_data);
$: gpxData = trails.map((t) => {
return { id: t.id, gpx: t.expand.gpx_data };
});
$: if (
gpxData &&
gpxGroup &&
@@ -216,15 +219,20 @@
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
markerLayerGroup.clearLayers();
activeTrailIndex = null;
markers = [];
if (polyline._selected) {
activeTrailIndex = polyline.options.index;
activeTrailIndex = trails.findIndex(
(t) => t.id ==
polyline.options.id,
);
for (const waypoint of trails.at(activeTrailIndex!)?.expand
.waypoints ?? []) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(markerLayerGroup!);
markers.push(marker);
}
}
});
@@ -237,8 +245,8 @@
gpxGroup.on("route_loaded", ({ route }: { route: any }) => {
const trail = trails.find(
(t) =>
gpxGroup?._hashCode(t.expand.gpx_data) ==
route.options.hash,
t.id ==
route.options.id,
);
if (!trail) {
@@ -275,16 +283,28 @@
gpxGroup.addTo(map);
});
export function selectTrail(index: number) {
gpxGroup.select(index);
export function highlightTrail(id: string) {
gpxGroup?.highlightTrack(id);
}
export function openPopup(index: number) {
gpxGroup.openPopup(index);
export function unHighlightTrail(id: string) {
gpxGroup?.unHighlightTrack(id);
}
export function closePopup(index: number) {
gpxGroup.closePopup(index);
export function selectTrail(id: string) {
gpxGroup?.select(id);
}
export function resetSelection() {
gpxGroup?.resetSelection();
}
export function openPopup(id: string) {
gpxGroup?.openPopup(id);
}
export function closePopup(id: string) {
gpxGroup?.closePopup(id);
}
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {

View File

@@ -23,7 +23,7 @@
import { pb } from "$lib/pocketbase";
export let trail: Trail;
export let mode: "overview" | "map";
export let mode: "overview" | "map" | "list";
let openConfirmModal: () => void;
let openListSelectModal: () => void;

View File

@@ -21,7 +21,11 @@
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { createMarkerFromWaypoint, endIcon, startIcon } from "$lib/util/leaflet_util";
import {
createMarkerFromWaypoint,
endIcon,
startIcon,
} from "$lib/util/leaflet_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import type { Icon, Map, Marker } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
@@ -36,7 +40,7 @@
import ShareInfo from "../share_info.svelte";
export let trail: Trail;
export let mode: "overview" | "map" = "map";
export let mode: "overview" | "map" | "list" = "map";
export let markers: Marker[] = [];
const tabs = [
@@ -116,6 +120,10 @@
markers[i].openPopup();
}
function closeMarkerPopup(i: number) {
markers[i].closePopup();
}
async function toggleMapFullScreen() {
goto(`/map/trail/${trail.id!}`);
}
@@ -160,12 +168,19 @@
</script>
<div
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full"
class="trail-info-panel mx-auto {mode == 'list'
? ''
: 'border border-input-border rounded-3xl'} h-full"
style="max-width: min(100%, 64rem);"
>
<div class="trail-info-panel-header">
<section class="relative h-80">
<img class="w-full h-80 rounded-t-3xl" src={thumbnail} alt="" />
<img
class="w-full h-80 "
class:rounded-t-3xl={mode !== "list"}
src={thumbnail}
alt=""
/>
<div
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
></div>
@@ -186,7 +201,8 @@
</span>
{/if}
{#if trailIsShared}
<ShareInfo type="trail" subject={trail} large={true}></ShareInfo>
<ShareInfo type="trail" subject={trail} large={true}
></ShareInfo>
{/if}
</div>
{/if}
@@ -286,7 +302,7 @@
{#if activeTab == 1}
<ul>
{#each trail.expand.waypoints ?? [] as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(i)}>
<li on:mouseenter={() => openMarkerPopup(i)} on:mouseleave={() => closeMarkerPopup(i)}>
<WaypointCard {waypoint}></WaypointCard>
</li>
{/each}

View File

@@ -11,6 +11,8 @@
export let trail: Trail;
export let showDescription: boolean = true;
$: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
: "/imgs/default_thumbnail.webp";
@@ -51,9 +53,7 @@
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
{/if}
<h5>
<i class="fa fa-gauge mr-3"></i>{$_(
trail.difficulty ?? "?",
)}
<i class="fa fa-gauge mr-3"></i>{$_(trail.difficulty ?? "?")}
</h5>
</div>
@@ -74,10 +74,12 @@
)}</span
>
</div>
<p
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
>
{trail.description}
</p>
{#if showDescription}
<p
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
>
{trail.description}
</p>
{/if}
</div>
</li>

View File

@@ -32,6 +32,7 @@
"completed": "Abgeschlossen",
"completion-status": "Abschlussstatus",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Mitwirken",
"copy-link": "Link kopieren",
@@ -155,6 +156,7 @@
"profile": "Profil",
"public": "Öffentlich",
"radius": "Radius",
"read-more": "",
"register": "Registrieren",
"removed-trail-from": "Route entfernt aus",
"required": "Pflichtfeld",

View File

@@ -32,6 +32,7 @@
"completed": "Completed",
"completion-status": "Completion Status",
"confirm": "Confirm",
"confirm-deletion": "Confirm Deletion",
"confirm-share": "Confirm share",
"contribute": "Contribute",
"copy-link": "Copy Link",
@@ -155,6 +156,7 @@
"profile": "Profile",
"public": "Public",
"radius": "Radius",
"read-more": "Read more",
"register": "Register",
"removed-trail-from": "Removed trail from",
"required": "Required",

View File

@@ -32,6 +32,7 @@
"completed": "Compléter",
"completion-status": "L'état d'achèvement",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Contribuer",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profile",
"public": "Publique",
"radius": "Rayon",
"read-more": "",
"register": "S'enregistrer",
"removed-trail-from": "Enlever l'itinéraire de",
"required": "Requis",

View File

@@ -32,6 +32,7 @@
"completed": "Teljesítve",
"completion-status": "Befejezés állapota",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Hozzájárulás",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profil",
"public": "Publikus",
"radius": "Átmérő",
"read-more": "",
"register": "Regisztráció",
"removed-trail-from": "Eltávolított nyomvonal a",
"required": "Kötelező",

View File

@@ -32,6 +32,7 @@
"completed": "Completato",
"completion-status": "Stato di completamento",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Contribuisci",
"copy-link": "Copia link",
@@ -155,6 +156,7 @@
"profile": "Profilo",
"public": "Pubblico",
"radius": "Raggio",
"read-more": "",
"register": "Registrati",
"removed-trail-from": "Percorso rimosso da",
"required": "Obbligatorio",

View File

@@ -32,6 +32,7 @@
"completed": "Voltooid",
"completion-status": "Voltooiingsstatus",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Bijdragen",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profiel",
"public": "Openbaar",
"radius": "Straal",
"read-more": "",
"register": "Registreren",
"removed-trail-from": "De wandelroute is verwijderd van",
"required": "Verplicht",

View File

@@ -32,6 +32,7 @@
"completed": "Zakończono",
"completion-status": "Stan ukończenia",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Kontrybuuj",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profil",
"public": "Publiczny",
"radius": "Promień",
"read-more": "",
"register": "Zarejestruj",
"removed-trail-from": "Usunięto ścieżkę z",
"required": "Wymagane",

View File

@@ -32,6 +32,7 @@
"completed": "Completada",
"completion-status": "Status de conclusão",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Contribuir",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Perfil",
"public": "Público",
"radius": "Raio",
"read-more": "",
"register": "Registo",
"removed-trail-from": "Trilha removida de",
"required": "Obrigatório",

View File

@@ -32,6 +32,7 @@
"completed": "已完成",
"completion-status": "完成状态",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "贡献",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "个人资料",
"public": "公开",
"radius": "半径",
"read-more": "",
"register": "注册",
"removed-trail-from": "路线已删除自",
"required": "必填",

View File

@@ -9,7 +9,7 @@ export class List {
avatar?: string;
trails?: string[];
expand?: {
trails: Trail[]
trails?: Trail[]
list_share_via_list?: ListShare[]
}

View File

@@ -7,8 +7,8 @@ import { writable, type Writable } from "svelte/store";
import { fetchGPX } from "./trail_store";
export const lists: Writable<List[]> = writable([])
export const list: Writable<List> = writable(new List("", []))
export const list: Writable<List | null> = writable(null)
export const listTrail: Writable<Trail | null> = writable(null);
export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/list?' + new URLSearchParams({
@@ -26,7 +26,7 @@ export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | UR
lists.set(fetchedLists);
if (fetchedLists.length > 0) {
list.set(fetchedLists[0])
// list.set(fetchedLists[0])
}
return fetchedLists;
@@ -39,7 +39,7 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
})
const response = await r.json()
for (const trail of response.expand.trails) {
for (const trail of response.expand?.trails ?? []) {
const gpxData: string = await fetchGPX(trail);
trail.expand.gpx_data = gpxData;
}

View File

@@ -140,40 +140,62 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
},
select(index) {
if (index > this._routes.length - 1) {
return
highlightTrack(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this.setSelection(this._routes[index])
route.eachLayer((l) => {
this.highlight(route, l)
})
},
openPopup(index) {
if (index > this._routes.length - 1) {
return
unHighlightTrack(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this._routes[index].openPopup();
route.eachLayer((l) => {
this.unhighlight(route, l)
})
},
closePopup(index) {
if (index > this._routes.length - 1) {
return
select(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this.setSelection(route)
},
this._routes[index].closePopup();
resetSelection() {
this.setSelection(this._selected)
if (this.options.flyToBounds) {
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
}
},
openPopup(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.openPopup();
},
closePopup(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.closePopup();
},
_addTrack: function (track) {
if (track instanceof Object) {
this._loadGeoJSON(track);
} else if (track !== undefined) {
this._elevation._parseFromString(track)
.then(geojson => this._loadGeoJSON(geojson, this._hashCode(track), track.split('/').pop().split('#')[0].split('?')[0]))
}
},
this._elevation._parseFromString(track.gpx)
.then(geojson => this._loadGeoJSON(geojson, track.id, track.gpx.split('/').pop().split('#')[0].split('?')[0]))
_hashCode: (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0),
},
clear: function () {
this._elevation.clear()
@@ -197,9 +219,9 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
}
},
_loadGeoJSON: function (geojson, hash, fallbackName) {
_loadGeoJSON: function (geojson, id, fallbackName) {
if (geojson) {
geojson.hash = hash
geojson.id = id
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
this._loadRoute(geojson);
}
@@ -208,7 +230,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
_loadRoute: function (data) {
if (!data) return;
var line_style = {
color: this._hashToColor(data.hash),
color: this._stringToColor(data.id),
opacity: 0.75,
weight: 5,
distanceMarkers: this.options.distanceMarkers_options,
@@ -220,7 +242,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
distanceMarkers: line_style.distanceMarkers,
originalStyle: line_style,
isGroupLayer: true,
hash: data.hash,
id: data.id,
filter: feature => feature.geometry.type != "Point",
});
@@ -351,23 +373,20 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
}
},
_hashToColor(hash) {
// Ensure hash is a positive integer
hash = Math.abs(hash);
_stringToColor(input) {
// Define the maximum brightness to ensure the color is not too light
const maxBrightness = 250;
const maxBrightness = 200;
// Convert the hash to a color in a reduced RGB range to prevent light and green colors
const r = (hash >> 16) & 0xFF; // Extract the red component
const g = (hash >> 8) & 0xFF; // Extract the green component
const b = hash & 0xFF; // Extract the blue component
// Divide the string into 3 parts (5 characters each)
const redPart = input.slice(0, 5);
const greenPart = input.slice(5, 10);
const bluePart = input.slice(10, 15);
// Adjust the green value to avoid greenish colors
// Reducing the green component significantly to avoid strong green shades
const green = Math.floor((g / 255) * (maxBrightness * 0.5)); // Reduce green component range
const red = Math.floor((r / 255) * maxBrightness);
const blue = Math.floor((b / 255) * maxBrightness);
// Calculate the red, green, and blue values by summing the char codes of the parts
const red = Math.floor((redPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
const greenRaw = greenPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256;
const green = Math.floor((greenRaw * 0.5) * (maxBrightness / 255)); // Reduce green to avoid greenish colors
const blue = Math.floor((bluePart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
// Format as HEX color
return `#${(1 << 24 | red << 16 | green << 8 | blue).toString(16).slice(1).toUpperCase()}`;

View File

@@ -4,19 +4,18 @@
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
import ConfirmModal from "$lib/components/confirm_modal.svelte";
import ListCard from "$lib/components/list/list_card.svelte";
import ListModal from "$lib/components/list/list_modal.svelte";
import ListPanel from "$lib/components/list/list_panel.svelte";
import ListShareModal from "$lib/components/list/list_share_modal.svelte";
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte";
import { List } from "$lib/models/list";
import type { TrailFilter } from "$lib/models/trail";
import type { Trail } from "$lib/models/trail";
import {
list,
lists,
lists_create,
lists_delete,
lists_index,
lists_update,
} from "$lib/stores/list_store";
import { fetchGPX } from "$lib/stores/trail_store";
import "$lib/vendor/leaflet-elevation/src/index.css";
@@ -27,136 +26,171 @@
import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n";
let openListModal: () => void;
let openConfirmModal: () => void;
let openShareModal: () => void;
let listToBeDeleted: List | null = null;
let map: Map;
let mapWithElevationMultiple: MapWithElevationMultiple;
let markers: any[];
let showMap: boolean = true;
let selectedList: List | null = null;
let selectedTrail: Trail | null = null;
onMount(() => {
if ($page.url.searchParams.get("list")) {
const listToFocus = $lists.find((l) => l.id == $page.url.searchParams.get("list"));
if(listToFocus) {
const listToFocus = $lists.find(
(l) => l.id == $page.url.searchParams.get("list"),
);
if (listToFocus) {
setCurrentList(listToFocus);
}
}
});
async function toggleMap() {
showMap = !showMap;
if (showMap) {
setCurrentList($list);
await tick();
map.invalidateSize();
}
}
async function saveList(e: CustomEvent<{ list: List; avatar?: File }>) {
const result = e.detail;
if (result.list.id) {
await lists_update(result.list, result.avatar);
} else {
await lists_create(result.list, result.avatar);
}
await lists_index();
}
async function handleDropdownClick(
e: CustomEvent<DropdownItem>,
currentList: List,
) {
async function handleDropdownClick(e: CustomEvent<DropdownItem>) {
const item = e.detail;
if (!selectedList) {
return;
}
if (item.value == "share") {
list.set(currentList);
await tick();
openShareModal();
} else if (item.value == "edit") {
goto("/lists/edit/" + currentList.id);
goto("/lists/edit/" + selectedList.id);
} else if (item.value == "delete") {
openConfirmModal();
listToBeDeleted = currentList;
}
}
async function deleteList() {
if (!listToBeDeleted) {
if (!selectedList) {
return;
}
await lists_delete(listToBeDeleted);
await lists_delete(selectedList);
await lists_index();
listToBeDeleted = null;
selectedList = null;
}
async function setCurrentList(item: List) {
if (item.expand && item.expand.trails.length > 0) {
if (
item.expand &&
item.expand.trails &&
item.expand.trails.length > 0
) {
for (const trail of item.expand.trails) {
const gpxData: string = await fetchGPX(trail);
trail.expand.gpx_data = gpxData;
}
}
list.set(item);
selectedList = item;
}
function back() {
if (selectedTrail) {
selectedTrail = null;
mapWithElevationMultiple.resetSelection();
} else if (selectedList) {
selectedList = null;
map.flyTo([0, 0], 4, {
duration: 0.25,
easeLinearity: 0.25,
noMoveStart: true,
});
}
}
function selectTrail(trail: Trail) {
selectedTrail = trail;
mapWithElevationMultiple.selectTrail(trail.id!);
window.scrollTo({ top: 0 });
}
function highlightTrail(trail: Trail) {
mapWithElevationMultiple.highlightTrail(trail.id!);
}
function unHighlightTrail(trail: Trail) {
mapWithElevationMultiple.unHighlightTrail(trail.id!);
}
</script>
<svelte:head>
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
</svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 mx-4">
<ul
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full"
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 md:mx-4">
<div
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full w-full order-1 md:order-none"
>
<div
class="flex gap-x-4 items-center justify-between p-4 bg-background z-50 rounded-xl"
>
<button
class="btn-icon"
class:btn-disabled={!selectedList}
disabled={!selectedList}
on:click={back}><i class="fa fa-arrow-left"></i></button
>
<a class="btn-primary btn-large text-center" href="/lists/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-list")}</a
>
<button type="button" class="btn-icon" on:click={toggleMap}
<!-- <button type="button" class="btn-icon" on:click={toggleMap}
><i class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"
></i></button
>
> -->
</div>
<hr class="border-separator mb-2" />
<hr class="border-separator" />
<div class="px-4">
{#each $lists as item, i}
<li
class="list-list-item my-1"
on:click={() => setCurrentList(item)}
role="presentation"
>
<ListCard
list={item}
on:change={(e) => handleDropdownClick(e, item)}
active={item.id === $list.id}
></ListCard>
</li>
{/each}
<div>
{#if !selectedList}
<div class="px-4 mt-2">
{#each $lists as item, i}
<div
class="list-list-item my-1"
on:click={() => setCurrentList(item)}
role="presentation"
>
<ListCard list={item}></ListCard>
</div>
{/each}
</div>
{: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)}
></ListPanel>
{:else if selectedList && selectedTrail}
<TrailInfoPanel trail={selectedTrail} mode="list" {markers}
></TrailInfoPanel>
{/if}
</div>
</ul>
<div id="trail-map" class="sticky top-[62px]" class:hidden={!showMap}>
</div>
<div id="trail-map" class="md:sticky md:top-[62px]" class:hidden={!showMap}>
<MapWithElevationMultiple
trails={$list.expand?.trails ?? []}
trails={selectedList?.expand?.trails ?? []}
bind:map
bind:this={mapWithElevationMultiple}
bind:markers
options={{ itinerary: true, flyToBounds: true }}
></MapWithElevationMultiple>
</div>
<div class="min-w-0" class:hidden={showMap}>
<TrailList trails={$list.expand?.trails ?? []}></TrailList>
<TrailList trails={selectedList?.expand?.trails ?? []}></TrailList>
</div>
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
<ConfirmModal
text={$_("delete-list-confirm")}
bind:openModal={openConfirmModal}
on:confirm={deleteList}
></ConfirmModal>
<ListShareModal list={$list} bind:openShareModal></ListShareModal>
{#if selectedList}
<ListShareModal list={selectedList} bind:openShareModal
></ListShareModal>
{/if}
</main>
<style>

View File

@@ -126,14 +126,17 @@
async function handleSearchClick(item: SearchItem) {
const trail = await trails_show(item.value, true);
$form.trails?.push(trail.id!);
$form.expand!.trails = [...$form.expand!.trails, trail];
$form.expand = {
trails: [...($form.expand?.trails ?? []), trail],
list_share_via_list: $form.expand?.list_share_via_list,
};
}
function deleteTrail(trail: Trail) {
$form.trails = $form.trails?.filter((id) => id !== trail.id);
$form.expand!.trails = $form.expand!.trails.filter(
$form.expand!.trails = $form.expand!.trails!.filter(
(t) => t.id !== trail.id,
);
);
}
async function findNewTrailShares() {
@@ -174,6 +177,26 @@
text: $_("list-saved-successfully"),
});
}
function moveTrail(trail: Trail, index: number, direction: 1 | -1) {
(document?.activeElement as HTMLElement).blur()
if (!$form.trails || !$form.expand || !$form.expand.trails) {
return;
}
const previousTrail = $form.expand?.trails?.at(index + direction);
const previousTrailId = $form.trails?.at(index + direction);
if (!previousTrail || !previousTrailId) {
return;
}
$form.expand!.trails![index] = previousTrail;
$form.expand!.trails![index + direction] = trail;
$form.trails[index] = previousTrailId;
$form.trails[index + direction] = trail.id!;
}
</script>
<svelte:head>
@@ -181,6 +204,7 @@
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer</title
>
</svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
<form
id="list-form"
@@ -246,14 +270,15 @@
placeholder="{$_('search-trails')}..."
items={searchDropdownItems}
></Search>
{#if $form.expand?.trails.length}
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if $form.expand?.trails?.length}
{#each $form.expand?.trails ?? [] as trail, i}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
class:border-primary={i == activeTrailIndex}
on:click={() => map.selectTrail(i)}
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 ?? "")}
>
<div class="shrink-0">
<img
@@ -268,17 +293,14 @@
/>
</div>
<div class="basis-full">
<div class="flex items-center justify-between">
<h4 class="font-semibold text-lg">
{trail.name}
</h4>
<span class="text-sm"
><i class="fa fa-gauge mr-2"></i>{$_(
trail.difficulty ?? "?",
)}</span
>
</div>
<h4 class="font-semibold text-lg">
{trail.name}
</h4>
<span class="text-sm"
><i class="fa fa-gauge mr-2"></i>{$_(
trail.difficulty ?? "?",
)}</span
>
<div class="flex mt-1 gap-4 text-sm text-gray-500">
<span
><i class="fa fa-left-right mr-2"
@@ -296,12 +318,34 @@
>
</div>
</div>
<button
type="button"
class="btn-icon text-red-500"
on:click={() => deleteTrail(trail)}
><i class="fa fa-trash"></i></button
>
<div class="basis-0">
{#if i > 0}
<button
on:click|stopPropagation={() =>
moveTrail(trail, i, -1)}
type="button"
class="btn-icon"
>
<i class="fa fa-chevron-up"></i>
</button>
{/if}
{#if i < $form.expand?.trails?.length - 1}
<button
on:click|stopPropagation={() =>
moveTrail(trail, i, 1)}
type="button"
class="btn-icon"
>
<i class="fa fa-chevron-down"></i>
</button>
{/if}
<button
type="button"
class="btn-icon text-red-500"
on:click={() => deleteTrail(trail)}
><i class="fa fa-trash"></i></button
>
</div>
</div>
{/each}
{:else}
@@ -328,7 +372,7 @@
</main>
<ConfirmModal
text={$_('list-share-warning-update')}
text={$_("list-share-warning-update")}
title={$_("confirm-share")}
action="confirm"
bind:openModal={openConfirmModal}
@@ -337,7 +381,8 @@
<style>
@media only screen and (min-width: 768px) {
#trail-map, #list-form {
#trail-map,
#list-form {
height: calc(100vh - 124px);
}
}

View File

@@ -20,7 +20,7 @@ export const load: Load = async ({ params, fetch, data }) => {
return { list: list, previewUrl: previewURL }
} catch (e) {
} catch (e) {
if (e instanceof ClientResponseError) {
return error(e.status as any, e.message)
}

View File

@@ -104,12 +104,12 @@
);
}
function handleTrailCardMouseEnter(index: number) {
mapWithElevation.openPopup(index)
function handleTrailCardMouseEnter(trail: Trail) {
mapWithElevation.openPopup(trail.id!)
}
function handleTrailCardMouseLeave(index: number) {
mapWithElevation.closePopup(index)
function handleTrailCardMouseLeave(trail: Trail) {
mapWithElevation.closePopup(trail.id!)
}
async function handleFilterUpdate(filter: TrailFilter) {
@@ -210,8 +210,8 @@
<a href="map/trail/{trail.id}">
<TrailCard
{trail}
on:mouseenter={() => handleTrailCardMouseEnter(i)}
on:mouseleave={() => handleTrailCardMouseLeave(i)}
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
on:mouseleave={() => handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
{/each}