diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c199691..4aaa14d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/src/assets/guides/wanderer_lists.gif b/docs/src/assets/guides/wanderer_lists.gif index 2fcd89cd..18092f6f 100644 Binary files a/docs/src/assets/guides/wanderer_lists.gif and b/docs/src/assets/guides/wanderer_lists.gif differ diff --git a/docs/src/assets/guides/wanderer_lists_edit.png b/docs/src/assets/guides/wanderer_lists_edit.png new file mode 100644 index 00000000..96784402 Binary files /dev/null and b/docs/src/assets/guides/wanderer_lists_edit.png differ diff --git a/docs/src/content/docs/api-reference/list-share.md b/docs/src/content/docs/api-reference/list-share.md new file mode 100644 index 00000000..544c3351 --- /dev/null +++ b/docs/src/content/docs/api-reference/list-share.md @@ -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 + diff --git a/docs/src/content/docs/guides/lists.md b/docs/src/content/docs/guides/lists.md index c4661e91..50b4336e 100644 --- a/docs/src/content/docs/guides/lists.md +++ b/docs/src/content/docs/guides/lists.md @@ -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. 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 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 sharing a trail. 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. \ No newline at end of file +To edit or delete a list first select it from the overview. Then you can select the respective entry from the ⋮ menu. \ No newline at end of file diff --git a/web/src/lib/components/confirm_modal.svelte b/web/src/lib/components/confirm_modal.svelte index 14d6616f..b876da7d 100644 --- a/web/src/lib/components/confirm_modal.svelte +++ b/web/src/lib/components/confirm_modal.svelte @@ -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"; diff --git a/web/src/lib/components/list/list_card.svelte b/web/src/lib/components/list/list_card.svelte index 5823359c..eb7e21e4 100644 --- a/web/src/lib/components/list/list_card.svelte +++ b/web/src/lib/components/list/list_card.svelte @@ -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" }] - : []), - ];
{/if} - {#if dropdownItems.length} - - {/if}

- {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 }, })}

- 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({ - 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) ?? ""; - } - - - - -

-
- - -
- diff --git a/web/src/lib/components/list/list_panel.svelte b/web/src/lib/components/list/list_panel.svelte new file mode 100644 index 00000000..1f6c8051 --- /dev/null +++ b/web/src/lib/components/list/list_panel.svelte @@ -0,0 +1,156 @@ + + +
+ {#if listIsShared} +
+ +
+ {/if} + + {#if dropdownItems.length} +
+ +
+ {/if} + {#if list.avatar} + avatar + {:else} +
+ +
+ {/if} +
+ +
+

{list.name}

+ +
+
+ {formatDistance( + cumulativeDistance, + )} + {formatElevation( + cumulativeElevationGain, + )} + {formatTimeHHMM( + cumulativeDuration, + )} +
+
+

+ {!fullDescription + ? list.description?.substring(0, 100) + : list.description} + {#if (list.description?.length ?? 0) > 100 && !fullDescription} + + {/if} +

+
+ {list.trails?.length ?? 0} + {$_("trail", { values: { n: list.trails?.length ?? 0 } })} +
+
+ {#each list.expand?.trails ?? [] as trail, i} +
handleTrailSelect(trail, i)} + on:mouseenter={() => handleTrailMouseEnter(trail, i)} + on:mouseleave={() => handleTrailMouseLeave(trail, i)} + > + +
+ {/each} +
+
diff --git a/web/src/lib/components/trail/map_with_elevation_multiple.svelte b/web/src/lib/components/trail/map_with_elevation_multiple.svelte index bf0bf300..dc1c7594 100644 --- a/web/src/lib/components/trail/map_with_elevation_multiple.svelte +++ b/web/src/lib/components/trail/map_with_elevation_multiple.svelte @@ -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) { diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index 7655e686..bd031764 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -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; diff --git a/web/src/lib/components/trail/trail_info_panel.svelte b/web/src/lib/components/trail/trail_info_panel.svelte index 669d6308..0f90a0f7 100644 --- a/web/src/lib/components/trail/trail_info_panel.svelte +++ b/web/src/lib/components/trail/trail_info_panel.svelte @@ -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 @@
- +
@@ -186,7 +201,8 @@ {/if} {#if trailIsShared} - + {/if}
{/if} @@ -286,7 +302,7 @@ {#if activeTab == 1}
@@ -74,10 +74,12 @@ )} -

- {trail.description} -

+ {#if showDescription} +

+ {trail.description} +

+ {/if} diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json index 80e9b8e3..74734789 100644 --- a/web/src/lib/i18n/locales/de.json +++ b/web/src/lib/i18n/locales/de.json @@ -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", diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index 36b421f3..5dbf3adf 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -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", diff --git a/web/src/lib/i18n/locales/fr.json b/web/src/lib/i18n/locales/fr.json index 149969ff..6f4633e4 100644 --- a/web/src/lib/i18n/locales/fr.json +++ b/web/src/lib/i18n/locales/fr.json @@ -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", diff --git a/web/src/lib/i18n/locales/hu.json b/web/src/lib/i18n/locales/hu.json index 2eddc305..22c3f598 100644 --- a/web/src/lib/i18n/locales/hu.json +++ b/web/src/lib/i18n/locales/hu.json @@ -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ő", diff --git a/web/src/lib/i18n/locales/it.json b/web/src/lib/i18n/locales/it.json index b84398b0..a9101ce0 100644 --- a/web/src/lib/i18n/locales/it.json +++ b/web/src/lib/i18n/locales/it.json @@ -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", diff --git a/web/src/lib/i18n/locales/nl.json b/web/src/lib/i18n/locales/nl.json index f31fb4e1..d0b46828 100644 --- a/web/src/lib/i18n/locales/nl.json +++ b/web/src/lib/i18n/locales/nl.json @@ -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", diff --git a/web/src/lib/i18n/locales/pl.json b/web/src/lib/i18n/locales/pl.json index 553d5060..84766d90 100644 --- a/web/src/lib/i18n/locales/pl.json +++ b/web/src/lib/i18n/locales/pl.json @@ -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", diff --git a/web/src/lib/i18n/locales/pt.json b/web/src/lib/i18n/locales/pt.json index 19b61a4a..f9e7fa46 100644 --- a/web/src/lib/i18n/locales/pt.json +++ b/web/src/lib/i18n/locales/pt.json @@ -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", diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index 906eeb84..8a0b98be 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -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": "必填", diff --git a/web/src/lib/models/list.ts b/web/src/lib/models/list.ts index 60c2861c..f7d196ad 100644 --- a/web/src/lib/models/list.ts +++ b/web/src/lib/models/list.ts @@ -9,7 +9,7 @@ export class List { avatar?: string; trails?: string[]; expand?: { - trails: Trail[] + trails?: Trail[] list_share_via_list?: ListShare[] } diff --git a/web/src/lib/stores/list_store.ts b/web/src/lib/stores/list_store.ts index efc2845c..15457ec1 100644 --- a/web/src/lib/stores/list_store.ts +++ b/web/src/lib/stores/list_store.ts @@ -7,8 +7,8 @@ import { writable, type Writable } from "svelte/store"; import { fetchGPX } from "./trail_store"; export const lists: Writable = writable([]) -export const list: Writable = writable(new List("", [])) - +export const list: Writable = writable(null) +export const listTrail: Writable = writable(null); export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = 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; } diff --git a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js index a38430a7..63da6a6e 100644 --- a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js +++ b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js @@ -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()}`; diff --git a/web/src/routes/lists/+page.svelte b/web/src/routes/lists/+page.svelte index aa02221c..490119ac 100644 --- a/web/src/routes/lists/+page.svelte +++ b/web/src/routes/lists/+page.svelte @@ -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, - currentList: List, - ) { + async function handleDropdownClick(e: CustomEvent) { 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!); } {$_("list", { values: { n: 2 } })} | wanderer -
-
    +
    + {$_("new-list")} - + > -->
    -
    +
    -
    - {#each $lists as item, i} -
  • setCurrentList(item)} - role="presentation" - > - handleDropdownClick(e, item)} - active={item.id === $list.id} - > -
  • - {/each} +
    + {#if !selectedList} +
    + {#each $lists as item, i} +
    setCurrentList(item)} + role="presentation" + > + +
    + {/each} +
    + {:else if selectedList && !selectedTrail} + highlightTrail(e.detail.trail)} + on:mouseleave={(e) => unHighlightTrail(e.detail.trail)} + on:change={(e) => handleDropdownClick(e)} + on:click={(e) => selectTrail(e.detail.trail)} + > + {:else if selectedList && selectedTrail} + + {/if}
    -
-
+
+
- +
- - + {#if selectedList} + + {/if}