adds docs
This commit is contained in:
@@ -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
|
# v0.8.2
|
||||||
## Features
|
## Features
|
||||||
- The pagination is now available at top and bottom
|
- 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 |
BIN
docs/src/assets/guides/wanderer_lists_edit.png
Normal file
BIN
docs/src/assets/guides/wanderer_lists_edit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 MiB |
391
docs/src/content/docs/api-reference/list-share.md
Normal file
391
docs/src/content/docs/api-reference/list-share.md
Normal 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
|
||||||
|
|
||||||
@@ -3,18 +3,27 @@ title: Lists
|
|||||||
description: What are lists and how can we use them?
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Create a list
|
## 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
|
## 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.
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
export let openModal: (() => void) | undefined = undefined;
|
export let openModal: (() => void) | undefined = undefined;
|
||||||
export let closeModal: (() => 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 text: string;
|
||||||
export let action: string = "delete";
|
export let action: string = "delete";
|
||||||
|
|
||||||
|
|||||||
@@ -6,43 +6,28 @@
|
|||||||
formatElevation,
|
formatElevation,
|
||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import ShareInfo from "../share_info.svelte";
|
import ShareInfo from "../share_info.svelte";
|
||||||
import { currentUser } from "$lib/stores/user_store";
|
|
||||||
export let list: List;
|
export let list: List;
|
||||||
export let active: boolean = false;
|
export let active: boolean = false;
|
||||||
|
|
||||||
$: cumulativeDistance = list.expand?.trails.reduce(
|
$: cumulativeDistance = list.expand?.trails?.reduce(
|
||||||
(s, b) => s + b.distance!,
|
(s, b) => s + b.distance!,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
$: cumulativeElevationGain = list.expand?.trails.reduce(
|
$: cumulativeElevationGain = list.expand?.trails?.reduce(
|
||||||
(s, b) => s + b.elevation_gain!,
|
(s, b) => s + b.elevation_gain!,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
$: cumulativeDuration = list.expand?.trails.reduce(
|
$: cumulativeDuration = list.expand?.trails?.reduce(
|
||||||
(s, b) => s + b.duration!,
|
(s, b) => s + b.duration!,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
$: listIsShared = (list.expand?.list_share_via_list?.length ?? 0) > 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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -72,9 +57,6 @@
|
|||||||
{#if listIsShared}
|
{#if listIsShared}
|
||||||
<ShareInfo type="list" subject={list}></ShareInfo>
|
<ShareInfo type="list" subject={list}></ShareInfo>
|
||||||
{/if}
|
{/if}
|
||||||
{#if dropdownItems.length}
|
|
||||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-1 gap-x-4 text-sm text-gray-500 whitespace-nowrap flex-wrap">
|
<div class="flex mt-1 gap-x-4 text-sm text-gray-500 whitespace-nowrap flex-wrap">
|
||||||
<span
|
<span
|
||||||
@@ -94,9 +76,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-500 mb-2">
|
<p class="text-sm text-gray-500 mb-2">
|
||||||
{list.expand?.trails.length ?? 0}
|
{list.expand?.trails?.length ?? 0}
|
||||||
{$_("trail", {
|
{$_("trail", {
|
||||||
values: { n: list.expand?.trails.length ?? 0 },
|
values: { n: list.expand?.trails?.length ?? 0 },
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -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>
|
|
||||||
156
web/src/lib/components/list/list_panel.svelte
Normal file
156
web/src/lib/components/list/list_panel.svelte
Normal 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>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
export let map: any | null = null;
|
export let map: any | null = null;
|
||||||
export let options: any = {};
|
export let options: any = {};
|
||||||
export let activeTrailIndex: number | null = 0;
|
export let activeTrailIndex: number | null = 0;
|
||||||
|
export let markers: any[] = [];
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -28,7 +29,9 @@
|
|||||||
|
|
||||||
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
|
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 (
|
$: if (
|
||||||
gpxData &&
|
gpxData &&
|
||||||
gpxGroup &&
|
gpxGroup &&
|
||||||
@@ -216,15 +219,20 @@
|
|||||||
|
|
||||||
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
|
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
|
||||||
markerLayerGroup.clearLayers();
|
markerLayerGroup.clearLayers();
|
||||||
|
|
||||||
activeTrailIndex = null;
|
activeTrailIndex = null;
|
||||||
|
markers = [];
|
||||||
|
|
||||||
if (polyline._selected) {
|
if (polyline._selected) {
|
||||||
activeTrailIndex = polyline.options.index;
|
activeTrailIndex = trails.findIndex(
|
||||||
|
(t) => t.id ==
|
||||||
|
polyline.options.id,
|
||||||
|
);
|
||||||
|
|
||||||
for (const waypoint of trails.at(activeTrailIndex!)?.expand
|
for (const waypoint of trails.at(activeTrailIndex!)?.expand
|
||||||
.waypoints ?? []) {
|
.waypoints ?? []) {
|
||||||
const marker = createMarkerFromWaypoint(L, waypoint);
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
marker.addTo(markerLayerGroup!);
|
marker.addTo(markerLayerGroup!);
|
||||||
|
markers.push(marker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -237,8 +245,8 @@
|
|||||||
gpxGroup.on("route_loaded", ({ route }: { route: any }) => {
|
gpxGroup.on("route_loaded", ({ route }: { route: any }) => {
|
||||||
const trail = trails.find(
|
const trail = trails.find(
|
||||||
(t) =>
|
(t) =>
|
||||||
gpxGroup?._hashCode(t.expand.gpx_data) ==
|
t.id ==
|
||||||
route.options.hash,
|
route.options.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!trail) {
|
if (!trail) {
|
||||||
@@ -275,16 +283,28 @@
|
|||||||
gpxGroup.addTo(map);
|
gpxGroup.addTo(map);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function selectTrail(index: number) {
|
export function highlightTrail(id: string) {
|
||||||
gpxGroup.select(index);
|
gpxGroup?.highlightTrack(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openPopup(index: number) {
|
export function unHighlightTrail(id: string) {
|
||||||
gpxGroup.openPopup(index);
|
gpxGroup?.unHighlightTrack(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closePopup(index: number) {
|
export function selectTrail(id: string) {
|
||||||
gpxGroup.closePopup(index);
|
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) {
|
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
|
|
||||||
export let trail: Trail;
|
export let trail: Trail;
|
||||||
export let mode: "overview" | "map";
|
export let mode: "overview" | "map" | "list";
|
||||||
|
|
||||||
let openConfirmModal: () => void;
|
let openConfirmModal: () => void;
|
||||||
let openListSelectModal: () => void;
|
let openListSelectModal: () => void;
|
||||||
|
|||||||
@@ -21,7 +21,11 @@
|
|||||||
formatElevation,
|
formatElevation,
|
||||||
formatTimeHHMM,
|
formatTimeHHMM,
|
||||||
} from "$lib/util/format_util";
|
} 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 "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import type { Icon, Map, Marker } from "leaflet";
|
import type { Icon, Map, Marker } from "leaflet";
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
@@ -36,7 +40,7 @@
|
|||||||
import ShareInfo from "../share_info.svelte";
|
import ShareInfo from "../share_info.svelte";
|
||||||
|
|
||||||
export let trail: Trail;
|
export let trail: Trail;
|
||||||
export let mode: "overview" | "map" = "map";
|
export let mode: "overview" | "map" | "list" = "map";
|
||||||
export let markers: Marker[] = [];
|
export let markers: Marker[] = [];
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -116,6 +120,10 @@
|
|||||||
markers[i].openPopup();
|
markers[i].openPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeMarkerPopup(i: number) {
|
||||||
|
markers[i].closePopup();
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleMapFullScreen() {
|
async function toggleMapFullScreen() {
|
||||||
goto(`/map/trail/${trail.id!}`);
|
goto(`/map/trail/${trail.id!}`);
|
||||||
}
|
}
|
||||||
@@ -160,12 +168,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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);"
|
style="max-width: min(100%, 64rem);"
|
||||||
>
|
>
|
||||||
<div class="trail-info-panel-header">
|
<div class="trail-info-panel-header">
|
||||||
<section class="relative h-80">
|
<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
|
<div
|
||||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||||
></div>
|
></div>
|
||||||
@@ -186,7 +201,8 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if trailIsShared}
|
{#if trailIsShared}
|
||||||
<ShareInfo type="trail" subject={trail} large={true}></ShareInfo>
|
<ShareInfo type="trail" subject={trail} large={true}
|
||||||
|
></ShareInfo>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -286,7 +302,7 @@
|
|||||||
{#if activeTab == 1}
|
{#if activeTab == 1}
|
||||||
<ul>
|
<ul>
|
||||||
{#each trail.expand.waypoints ?? [] as waypoint, i}
|
{#each trail.expand.waypoints ?? [] as waypoint, i}
|
||||||
<li on:mouseenter={() => openMarkerPopup(i)}>
|
<li on:mouseenter={() => openMarkerPopup(i)} on:mouseleave={() => closeMarkerPopup(i)}>
|
||||||
<WaypointCard {waypoint}></WaypointCard>
|
<WaypointCard {waypoint}></WaypointCard>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
export let trail: Trail;
|
export let trail: Trail;
|
||||||
|
|
||||||
|
export let showDescription: boolean = true;
|
||||||
|
|
||||||
$: thumbnail = trail.photos.length
|
$: thumbnail = trail.photos.length
|
||||||
? getFileURL(trail, trail.photos[trail.thumbnail])
|
? getFileURL(trail, trail.photos[trail.thumbnail])
|
||||||
: "/imgs/default_thumbnail.webp";
|
: "/imgs/default_thumbnail.webp";
|
||||||
@@ -51,9 +53,7 @@
|
|||||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||||
{/if}
|
{/if}
|
||||||
<h5>
|
<h5>
|
||||||
<i class="fa fa-gauge mr-3"></i>{$_(
|
<i class="fa fa-gauge mr-3"></i>{$_(trail.difficulty ?? "?")}
|
||||||
trail.difficulty ?? "?",
|
|
||||||
)}
|
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -74,10 +74,12 @@
|
|||||||
)}</span
|
)}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p
|
{#if showDescription}
|
||||||
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
|
<p
|
||||||
>
|
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
|
||||||
{trail.description}
|
>
|
||||||
</p>
|
{trail.description}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Abgeschlossen",
|
"completed": "Abgeschlossen",
|
||||||
"completion-status": "Abschlussstatus",
|
"completion-status": "Abschlussstatus",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Mitwirken",
|
"contribute": "Mitwirken",
|
||||||
"copy-link": "Link kopieren",
|
"copy-link": "Link kopieren",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"public": "Öffentlich",
|
"public": "Öffentlich",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
|
"read-more": "",
|
||||||
"register": "Registrieren",
|
"register": "Registrieren",
|
||||||
"removed-trail-from": "Route entfernt aus",
|
"removed-trail-from": "Route entfernt aus",
|
||||||
"required": "Pflichtfeld",
|
"required": "Pflichtfeld",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"completion-status": "Completion Status",
|
"completion-status": "Completion Status",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
"confirm-deletion": "Confirm Deletion",
|
||||||
"confirm-share": "Confirm share",
|
"confirm-share": "Confirm share",
|
||||||
"contribute": "Contribute",
|
"contribute": "Contribute",
|
||||||
"copy-link": "Copy Link",
|
"copy-link": "Copy Link",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"public": "Public",
|
"public": "Public",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
|
"read-more": "Read more",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"removed-trail-from": "Removed trail from",
|
"removed-trail-from": "Removed trail from",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Compléter",
|
"completed": "Compléter",
|
||||||
"completion-status": "L'état d'achèvement",
|
"completion-status": "L'état d'achèvement",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Contribuer",
|
"contribute": "Contribuer",
|
||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"public": "Publique",
|
"public": "Publique",
|
||||||
"radius": "Rayon",
|
"radius": "Rayon",
|
||||||
|
"read-more": "",
|
||||||
"register": "S'enregistrer",
|
"register": "S'enregistrer",
|
||||||
"removed-trail-from": "Enlever l'itinéraire de",
|
"removed-trail-from": "Enlever l'itinéraire de",
|
||||||
"required": "Requis",
|
"required": "Requis",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Teljesítve",
|
"completed": "Teljesítve",
|
||||||
"completion-status": "Befejezés állapota",
|
"completion-status": "Befejezés állapota",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Hozzájárulás",
|
"contribute": "Hozzájárulás",
|
||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"public": "Publikus",
|
"public": "Publikus",
|
||||||
"radius": "Átmérő",
|
"radius": "Átmérő",
|
||||||
|
"read-more": "",
|
||||||
"register": "Regisztráció",
|
"register": "Regisztráció",
|
||||||
"removed-trail-from": "Eltávolított nyomvonal a",
|
"removed-trail-from": "Eltávolított nyomvonal a",
|
||||||
"required": "Kötelező",
|
"required": "Kötelező",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Completato",
|
"completed": "Completato",
|
||||||
"completion-status": "Stato di completamento",
|
"completion-status": "Stato di completamento",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Contribuisci",
|
"contribute": "Contribuisci",
|
||||||
"copy-link": "Copia link",
|
"copy-link": "Copia link",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profilo",
|
"profile": "Profilo",
|
||||||
"public": "Pubblico",
|
"public": "Pubblico",
|
||||||
"radius": "Raggio",
|
"radius": "Raggio",
|
||||||
|
"read-more": "",
|
||||||
"register": "Registrati",
|
"register": "Registrati",
|
||||||
"removed-trail-from": "Percorso rimosso da",
|
"removed-trail-from": "Percorso rimosso da",
|
||||||
"required": "Obbligatorio",
|
"required": "Obbligatorio",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Voltooid",
|
"completed": "Voltooid",
|
||||||
"completion-status": "Voltooiingsstatus",
|
"completion-status": "Voltooiingsstatus",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Bijdragen",
|
"contribute": "Bijdragen",
|
||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profiel",
|
"profile": "Profiel",
|
||||||
"public": "Openbaar",
|
"public": "Openbaar",
|
||||||
"radius": "Straal",
|
"radius": "Straal",
|
||||||
|
"read-more": "",
|
||||||
"register": "Registreren",
|
"register": "Registreren",
|
||||||
"removed-trail-from": "De wandelroute is verwijderd van",
|
"removed-trail-from": "De wandelroute is verwijderd van",
|
||||||
"required": "Verplicht",
|
"required": "Verplicht",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Zakończono",
|
"completed": "Zakończono",
|
||||||
"completion-status": "Stan ukończenia",
|
"completion-status": "Stan ukończenia",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Kontrybuuj",
|
"contribute": "Kontrybuuj",
|
||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"public": "Publiczny",
|
"public": "Publiczny",
|
||||||
"radius": "Promień",
|
"radius": "Promień",
|
||||||
|
"read-more": "",
|
||||||
"register": "Zarejestruj",
|
"register": "Zarejestruj",
|
||||||
"removed-trail-from": "Usunięto ścieżkę z",
|
"removed-trail-from": "Usunięto ścieżkę z",
|
||||||
"required": "Wymagane",
|
"required": "Wymagane",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "Completada",
|
"completed": "Completada",
|
||||||
"completion-status": "Status de conclusão",
|
"completion-status": "Status de conclusão",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "Contribuir",
|
"contribute": "Contribuir",
|
||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "Perfil",
|
"profile": "Perfil",
|
||||||
"public": "Público",
|
"public": "Público",
|
||||||
"radius": "Raio",
|
"radius": "Raio",
|
||||||
|
"read-more": "",
|
||||||
"register": "Registo",
|
"register": "Registo",
|
||||||
"removed-trail-from": "Trilha removida de",
|
"removed-trail-from": "Trilha removida de",
|
||||||
"required": "Obrigatório",
|
"required": "Obrigatório",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"completion-status": "完成状态",
|
"completion-status": "完成状态",
|
||||||
"confirm": "",
|
"confirm": "",
|
||||||
|
"confirm-deletion": "",
|
||||||
"confirm-share": "",
|
"confirm-share": "",
|
||||||
"contribute": "贡献",
|
"contribute": "贡献",
|
||||||
"copy-link": "",
|
"copy-link": "",
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
"profile": "个人资料",
|
"profile": "个人资料",
|
||||||
"public": "公开",
|
"public": "公开",
|
||||||
"radius": "半径",
|
"radius": "半径",
|
||||||
|
"read-more": "",
|
||||||
"register": "注册",
|
"register": "注册",
|
||||||
"removed-trail-from": "路线已删除自",
|
"removed-trail-from": "路线已删除自",
|
||||||
"required": "必填",
|
"required": "必填",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class List {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
trails?: string[];
|
trails?: string[];
|
||||||
expand?: {
|
expand?: {
|
||||||
trails: Trail[]
|
trails?: Trail[]
|
||||||
list_share_via_list?: ListShare[]
|
list_share_via_list?: ListShare[]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { writable, type Writable } from "svelte/store";
|
|||||||
import { fetchGPX } from "./trail_store";
|
import { fetchGPX } from "./trail_store";
|
||||||
|
|
||||||
export const lists: Writable<List[]> = writable([])
|
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) {
|
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({
|
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);
|
lists.set(fetchedLists);
|
||||||
|
|
||||||
if (fetchedLists.length > 0) {
|
if (fetchedLists.length > 0) {
|
||||||
list.set(fetchedLists[0])
|
// list.set(fetchedLists[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchedLists;
|
return fetchedLists;
|
||||||
@@ -39,7 +39,7 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
|
|||||||
})
|
})
|
||||||
const response = await r.json()
|
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);
|
const gpxData: string = await fetchGPX(trail);
|
||||||
trail.expand.gpx_data = gpxData;
|
trail.expand.gpx_data = gpxData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,40 +140,62 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
select(index) {
|
highlightTrack(id) {
|
||||||
if (index > this._routes.length - 1) {
|
const route = this._routes.find(r => r.options.id == id)
|
||||||
return
|
if (!route) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
route.eachLayer((l) => {
|
||||||
this.setSelection(this._routes[index])
|
this.highlight(route, l)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
openPopup(index) {
|
unHighlightTrack(id) {
|
||||||
if (index > this._routes.length - 1) {
|
const route = this._routes.find(r => r.options.id == id)
|
||||||
return
|
if (!route) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
route.eachLayer((l) => {
|
||||||
this._routes[index].openPopup();
|
this.unhighlight(route, l)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
closePopup(index) {
|
select(id) {
|
||||||
if (index > this._routes.length - 1) {
|
const route = this._routes.find(r => r.options.id == id)
|
||||||
return
|
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) {
|
_addTrack: function (track) {
|
||||||
if (track instanceof Object) {
|
this._elevation._parseFromString(track.gpx)
|
||||||
this._loadGeoJSON(track);
|
.then(geojson => this._loadGeoJSON(geojson, track.id, track.gpx.split('/').pop().split('#')[0].split('?')[0]))
|
||||||
} else if (track !== undefined) {
|
|
||||||
this._elevation._parseFromString(track)
|
|
||||||
.then(geojson => this._loadGeoJSON(geojson, this._hashCode(track), track.split('/').pop().split('#')[0].split('?')[0]))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_hashCode: (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0),
|
},
|
||||||
|
|
||||||
clear: function () {
|
clear: function () {
|
||||||
this._elevation.clear()
|
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) {
|
if (geojson) {
|
||||||
geojson.hash = hash
|
geojson.id = id
|
||||||
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
|
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
|
||||||
this._loadRoute(geojson);
|
this._loadRoute(geojson);
|
||||||
}
|
}
|
||||||
@@ -208,7 +230,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
_loadRoute: function (data) {
|
_loadRoute: function (data) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
var line_style = {
|
var line_style = {
|
||||||
color: this._hashToColor(data.hash),
|
color: this._stringToColor(data.id),
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
weight: 5,
|
weight: 5,
|
||||||
distanceMarkers: this.options.distanceMarkers_options,
|
distanceMarkers: this.options.distanceMarkers_options,
|
||||||
@@ -220,7 +242,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
distanceMarkers: line_style.distanceMarkers,
|
distanceMarkers: line_style.distanceMarkers,
|
||||||
originalStyle: line_style,
|
originalStyle: line_style,
|
||||||
isGroupLayer: true,
|
isGroupLayer: true,
|
||||||
hash: data.hash,
|
id: data.id,
|
||||||
filter: feature => feature.geometry.type != "Point",
|
filter: feature => feature.geometry.type != "Point",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,23 +373,20 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_hashToColor(hash) {
|
_stringToColor(input) {
|
||||||
// Ensure hash is a positive integer
|
|
||||||
hash = Math.abs(hash);
|
|
||||||
|
|
||||||
// Define the maximum brightness to ensure the color is not too light
|
// 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
|
// Divide the string into 3 parts (5 characters each)
|
||||||
const r = (hash >> 16) & 0xFF; // Extract the red component
|
const redPart = input.slice(0, 5);
|
||||||
const g = (hash >> 8) & 0xFF; // Extract the green component
|
const greenPart = input.slice(5, 10);
|
||||||
const b = hash & 0xFF; // Extract the blue component
|
const bluePart = input.slice(10, 15);
|
||||||
|
|
||||||
// Adjust the green value to avoid greenish colors
|
// Calculate the red, green, and blue values by summing the char codes of the parts
|
||||||
// Reducing the green component significantly to avoid strong green shades
|
const red = Math.floor((redPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
|
||||||
const green = Math.floor((g / 255) * (maxBrightness * 0.5)); // Reduce green component range
|
const greenRaw = greenPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256;
|
||||||
const red = Math.floor((r / 255) * maxBrightness);
|
const green = Math.floor((greenRaw * 0.5) * (maxBrightness / 255)); // Reduce green to avoid greenish colors
|
||||||
const blue = Math.floor((b / 255) * maxBrightness);
|
const blue = Math.floor((bluePart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
|
||||||
|
|
||||||
// Format as HEX color
|
// Format as HEX color
|
||||||
return `#${(1 << 24 | red << 16 | green << 8 | blue).toString(16).slice(1).toUpperCase()}`;
|
return `#${(1 << 24 | red << 16 | green << 8 | blue).toString(16).slice(1).toUpperCase()}`;
|
||||||
|
|||||||
@@ -4,19 +4,18 @@
|
|||||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||||
import ListCard from "$lib/components/list/list_card.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 ListShareModal from "$lib/components/list/list_share_modal.svelte";
|
||||||
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.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 TrailList from "$lib/components/trail/trail_list.svelte";
|
||||||
import { List } from "$lib/models/list";
|
import { List } from "$lib/models/list";
|
||||||
import type { TrailFilter } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import {
|
import {
|
||||||
list,
|
list,
|
||||||
lists,
|
lists,
|
||||||
lists_create,
|
|
||||||
lists_delete,
|
lists_delete,
|
||||||
lists_index,
|
lists_index,
|
||||||
lists_update,
|
|
||||||
} from "$lib/stores/list_store";
|
} from "$lib/stores/list_store";
|
||||||
import { fetchGPX } from "$lib/stores/trail_store";
|
import { fetchGPX } from "$lib/stores/trail_store";
|
||||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
@@ -27,136 +26,171 @@
|
|||||||
import { onMount, tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let openListModal: () => void;
|
|
||||||
let openConfirmModal: () => void;
|
let openConfirmModal: () => void;
|
||||||
let openShareModal: () => void;
|
let openShareModal: () => void;
|
||||||
|
|
||||||
let listToBeDeleted: List | null = null;
|
|
||||||
|
|
||||||
let map: Map;
|
let map: Map;
|
||||||
|
let mapWithElevationMultiple: MapWithElevationMultiple;
|
||||||
|
let markers: any[];
|
||||||
let showMap: boolean = true;
|
let showMap: boolean = true;
|
||||||
|
|
||||||
|
let selectedList: List | null = null;
|
||||||
|
let selectedTrail: Trail | null = null;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($page.url.searchParams.get("list")) {
|
if ($page.url.searchParams.get("list")) {
|
||||||
const listToFocus = $lists.find((l) => l.id == $page.url.searchParams.get("list"));
|
const listToFocus = $lists.find(
|
||||||
if(listToFocus) {
|
(l) => l.id == $page.url.searchParams.get("list"),
|
||||||
|
);
|
||||||
|
if (listToFocus) {
|
||||||
setCurrentList(listToFocus);
|
setCurrentList(listToFocus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function toggleMap() {
|
async function handleDropdownClick(e: CustomEvent<DropdownItem>) {
|
||||||
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,
|
|
||||||
) {
|
|
||||||
const item = e.detail;
|
const item = e.detail;
|
||||||
|
|
||||||
|
if (!selectedList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (item.value == "share") {
|
if (item.value == "share") {
|
||||||
list.set(currentList);
|
|
||||||
await tick();
|
|
||||||
openShareModal();
|
openShareModal();
|
||||||
} else if (item.value == "edit") {
|
} else if (item.value == "edit") {
|
||||||
goto("/lists/edit/" + currentList.id);
|
goto("/lists/edit/" + selectedList.id);
|
||||||
} else if (item.value == "delete") {
|
} else if (item.value == "delete") {
|
||||||
openConfirmModal();
|
openConfirmModal();
|
||||||
listToBeDeleted = currentList;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteList() {
|
async function deleteList() {
|
||||||
if (!listToBeDeleted) {
|
if (!selectedList) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await lists_delete(listToBeDeleted);
|
await lists_delete(selectedList);
|
||||||
await lists_index();
|
await lists_index();
|
||||||
listToBeDeleted = null;
|
selectedList = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setCurrentList(item: List) {
|
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) {
|
for (const trail of item.expand.trails) {
|
||||||
const gpxData: string = await fetchGPX(trail);
|
const gpxData: string = await fetchGPX(trail);
|
||||||
trail.expand.gpx_data = gpxData;
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 mx-4">
|
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 md:mx-4">
|
||||||
<ul
|
<div
|
||||||
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full"
|
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full w-full order-1 md:order-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex gap-x-4 items-center justify-between p-4 bg-background z-50 rounded-xl"
|
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"
|
<a class="btn-primary btn-large text-center" href="/lists/edit/new"
|
||||||
><i class="fa fa-plus mr-2"></i>{$_("new-list")}</a
|
><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 class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"
|
||||||
></i></button
|
></i></button
|
||||||
>
|
> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-separator mb-2" />
|
<hr class="border-separator" />
|
||||||
|
|
||||||
<div class="px-4">
|
<div>
|
||||||
{#each $lists as item, i}
|
{#if !selectedList}
|
||||||
<li
|
<div class="px-4 mt-2">
|
||||||
class="list-list-item my-1"
|
{#each $lists as item, i}
|
||||||
on:click={() => setCurrentList(item)}
|
<div
|
||||||
role="presentation"
|
class="list-list-item my-1"
|
||||||
>
|
on:click={() => setCurrentList(item)}
|
||||||
<ListCard
|
role="presentation"
|
||||||
list={item}
|
>
|
||||||
on:change={(e) => handleDropdownClick(e, item)}
|
<ListCard list={item}></ListCard>
|
||||||
active={item.id === $list.id}
|
</div>
|
||||||
></ListCard>
|
{/each}
|
||||||
</li>
|
</div>
|
||||||
{/each}
|
{: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>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
<div id="trail-map" class="sticky top-[62px]" class:hidden={!showMap}>
|
<div id="trail-map" class="md:sticky md:top-[62px]" class:hidden={!showMap}>
|
||||||
<MapWithElevationMultiple
|
<MapWithElevationMultiple
|
||||||
trails={$list.expand?.trails ?? []}
|
trails={selectedList?.expand?.trails ?? []}
|
||||||
bind:map
|
bind:map
|
||||||
|
bind:this={mapWithElevationMultiple}
|
||||||
|
bind:markers
|
||||||
options={{ itinerary: true, flyToBounds: true }}
|
options={{ itinerary: true, flyToBounds: true }}
|
||||||
></MapWithElevationMultiple>
|
></MapWithElevationMultiple>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0" class:hidden={showMap}>
|
<div class="min-w-0" class:hidden={showMap}>
|
||||||
<TrailList trails={$list.expand?.trails ?? []}></TrailList>
|
<TrailList trails={selectedList?.expand?.trails ?? []}></TrailList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
text={$_("delete-list-confirm")}
|
text={$_("delete-list-confirm")}
|
||||||
bind:openModal={openConfirmModal}
|
bind:openModal={openConfirmModal}
|
||||||
on:confirm={deleteList}
|
on:confirm={deleteList}
|
||||||
></ConfirmModal>
|
></ConfirmModal>
|
||||||
<ListShareModal list={$list} bind:openShareModal></ListShareModal>
|
{#if selectedList}
|
||||||
|
<ListShareModal list={selectedList} bind:openShareModal
|
||||||
|
></ListShareModal>
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -126,14 +126,17 @@
|
|||||||
async function handleSearchClick(item: SearchItem) {
|
async function handleSearchClick(item: SearchItem) {
|
||||||
const trail = await trails_show(item.value, true);
|
const trail = await trails_show(item.value, true);
|
||||||
$form.trails?.push(trail.id!);
|
$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) {
|
function deleteTrail(trail: Trail) {
|
||||||
$form.trails = $form.trails?.filter((id) => id !== trail.id);
|
$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,
|
(t) => t.id !== trail.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findNewTrailShares() {
|
async function findNewTrailShares() {
|
||||||
@@ -174,6 +177,26 @@
|
|||||||
text: $_("list-saved-successfully"),
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -181,6 +204,7 @@
|
|||||||
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer</title
|
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer</title
|
||||||
>
|
>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
|
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
|
||||||
<form
|
<form
|
||||||
id="list-form"
|
id="list-form"
|
||||||
@@ -246,14 +270,15 @@
|
|||||||
placeholder="{$_('search-trails')}..."
|
placeholder="{$_('search-trails')}..."
|
||||||
items={searchDropdownItems}
|
items={searchDropdownItems}
|
||||||
></Search>
|
></Search>
|
||||||
{#if $form.expand?.trails.length}
|
{#if $form.expand?.trails?.length}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
{#each $form.expand?.trails ?? [] as trail, i}
|
{#each $form.expand?.trails ?? [] as trail, i}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
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}
|
class:bg-secondary-hover={i == activeTrailIndex}
|
||||||
on:click={() => map.selectTrail(i)}
|
role="presentation"
|
||||||
|
on:mouseenter={() => map.highlightTrail(trail.id ?? "")}
|
||||||
|
on:mouseleave={() => map.unHighlightTrail(trail.id ?? "")}
|
||||||
|
on:click={() => map.selectTrail(trail.id ?? "")}
|
||||||
>
|
>
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<img
|
<img
|
||||||
@@ -268,17 +293,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<div class="flex items-center justify-between">
|
<h4 class="font-semibold text-lg">
|
||||||
<h4 class="font-semibold text-lg">
|
{trail.name}
|
||||||
{trail.name}
|
</h4>
|
||||||
</h4>
|
<span class="text-sm"
|
||||||
<span class="text-sm"
|
><i class="fa fa-gauge mr-2"></i>{$_(
|
||||||
><i class="fa fa-gauge mr-2"></i>{$_(
|
trail.difficulty ?? "?",
|
||||||
trail.difficulty ?? "?",
|
)}</span
|
||||||
)}</span
|
>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex mt-1 gap-4 text-sm text-gray-500">
|
<div class="flex mt-1 gap-4 text-sm text-gray-500">
|
||||||
<span
|
<span
|
||||||
><i class="fa fa-left-right mr-2"
|
><i class="fa fa-left-right mr-2"
|
||||||
@@ -296,12 +318,34 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="basis-0">
|
||||||
type="button"
|
{#if i > 0}
|
||||||
class="btn-icon text-red-500"
|
<button
|
||||||
on:click={() => deleteTrail(trail)}
|
on:click|stopPropagation={() =>
|
||||||
><i class="fa fa-trash"></i></button
|
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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -328,7 +372,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
text={$_('list-share-warning-update')}
|
text={$_("list-share-warning-update")}
|
||||||
title={$_("confirm-share")}
|
title={$_("confirm-share")}
|
||||||
action="confirm"
|
action="confirm"
|
||||||
bind:openModal={openConfirmModal}
|
bind:openModal={openConfirmModal}
|
||||||
@@ -337,7 +381,8 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media only screen and (min-width: 768px) {
|
@media only screen and (min-width: 768px) {
|
||||||
#trail-map, #list-form {
|
#trail-map,
|
||||||
|
#list-form {
|
||||||
height: calc(100vh - 124px);
|
height: calc(100vh - 124px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const load: Load = async ({ params, fetch, data }) => {
|
|||||||
|
|
||||||
return { list: list, previewUrl: previewURL }
|
return { list: list, previewUrl: previewURL }
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ClientResponseError) {
|
if (e instanceof ClientResponseError) {
|
||||||
return error(e.status as any, e.message)
|
return error(e.status as any, e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,12 +104,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTrailCardMouseEnter(index: number) {
|
function handleTrailCardMouseEnter(trail: Trail) {
|
||||||
mapWithElevation.openPopup(index)
|
mapWithElevation.openPopup(trail.id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTrailCardMouseLeave(index: number) {
|
function handleTrailCardMouseLeave(trail: Trail) {
|
||||||
mapWithElevation.closePopup(index)
|
mapWithElevation.closePopup(trail.id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFilterUpdate(filter: TrailFilter) {
|
async function handleFilterUpdate(filter: TrailFilter) {
|
||||||
@@ -210,8 +210,8 @@
|
|||||||
<a href="map/trail/{trail.id}">
|
<a href="map/trail/{trail.id}">
|
||||||
<TrailCard
|
<TrailCard
|
||||||
{trail}
|
{trail}
|
||||||
on:mouseenter={() => handleTrailCardMouseEnter(i)}
|
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
|
||||||
on:mouseleave={() => handleTrailCardMouseLeave(i)}
|
on:mouseleave={() => handleTrailCardMouseLeave(trail)}
|
||||||
></TrailCard>
|
></TrailCard>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user