updates trail panel view
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
# v0.14.0
|
# v0.14.0
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> This release introduces significant updates, including the migration of the frontend to [Svelte 5](https://svelte.dev/blog/svelte-5-is-alive). While the migration has been rigorously tested, there is a possibility that some features may not function as expected. We encourage you to report any issues you encounter.
|
> This release introduces significant updates, including the migration of the frontend to [Svelte 5](https://svelte.dev/blog/svelte-5-is-alive). While the migration has been rigorously tested, there is a possibility that some features may not function as expected. We encourage you to report any issues you encounter. Additionally, the location search functionality has been transitioned from a locally hosted meilisearch index to nominatim. This upgrade offers substantially improved location search capabilities within wanderer. As a result, the custom meilisearch docker image (`flomp/wanderer-search`) is now deprecated. You can safely replace it with the official meilisearch image (`getmeili/meilisearch:v1.11.3`) in your `docker-compose.yml`.
|
||||||
|
|
||||||
> Additionally, the location search functionality has been transitioned from a locally hosted meilisearch index to nominatim. This upgrade offers substantially improved location search capabilities within wanderer. As a result, the custom meilisearch docker image (`flomp/wanderer-search`) is now deprecated. You can safely replace it with the official meilisearch image (`getmeili/meilisearch:v1.11.3`) in your `docker-compose.yml`.
|
|
||||||
|
|
||||||
## Maintenance
|
## Maintenance
|
||||||
- Migrates to Svelte 5
|
- Migrates to Svelte 5
|
||||||
|
|||||||
55
db/migrations/1737994649_updated_waypoints.go
Normal file
55
db/migrations/1737994649_updated_waypoints.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
new_distance_from_start := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "s1prb3fx",
|
||||||
|
"name": "distance_from_start",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": 0,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
}`), new_distance_from_start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(new_distance_from_start)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.Schema.RemoveField("s1prb3fx")
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,15 +27,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@apply p-4 rounded-t-lg border border-transparent
|
@apply mr-4 py-2 border-b-2 border-transparent rounded-t-lg
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
@apply border-menu-item-background-hover
|
@apply border-menu-item-background-focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-active {
|
||||||
|
@apply font-semibold
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-active:hover {
|
.tab-active:hover {
|
||||||
@apply bg-transparent
|
@apply border-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<div id="tabs" class="flex gap-2 overflow-x-auto relative {extraClasses}">
|
<div id="tabs" class="flex gap-2 overflow-x-auto relative {extraClasses}">
|
||||||
<div
|
<div
|
||||||
class="absolute h-full bg-menu-item-background-hover rounded-t-lg top-0 z-0"
|
class="tab-indicator absolute h-full rounded-t-lg top-0 z-0 border-b-2 border-content"
|
||||||
style="width: {indicatorWidth.current}px; left: {indicatorPosition.current}px;"
|
style="width: {indicatorWidth.current}px; left: {indicatorPosition.current}px;"
|
||||||
></div>
|
></div>
|
||||||
{#each tabs as tab, i}
|
{#each tabs as tab, i}
|
||||||
|
|||||||
@@ -940,12 +940,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.maplibregl-popup-content) {
|
:global(.maplibregl-popup-content) {
|
||||||
@apply bg-background rounded-md shadow-xl pt-5;
|
@apply bg-background rounded-md shadow-xl p-0 overflow-hidden pr-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.maplibregl-popup-close-button) {
|
:global(.maplibregl-popup-close-button) {
|
||||||
top: 4px;
|
top: 4px;
|
||||||
right: 8px;
|
right: 4px;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
padding-bottom: 2.5px;
|
padding-bottom: 2.5px;
|
||||||
@apply bg-menu-item-background-focus w-3 aspect-square rounded-full;
|
@apply bg-menu-item-background-focus w-3 aspect-square rounded-full;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
import EmptyStateWaypoint from "../empty_states/empty_state_waypoint.svelte";
|
import EmptyStateWaypoint from "../empty_states/empty_state_waypoint.svelte";
|
||||||
import EmptyStateDescription from "../empty_states/empty_state_description.svelte";
|
import EmptyStateDescription from "../empty_states/empty_state_description.svelte";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import TrailTimeline from "./trail_timeline.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trail: Trail;
|
trail: Trail;
|
||||||
@@ -54,10 +55,8 @@
|
|||||||
let { trail, mode = "map", markers = [], activeTab = 0 }: Props = $props();
|
let { trail, mode = "map", markers = [], activeTab = 0 }: Props = $props();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
$_("description"),
|
|
||||||
$_("waypoints", { values: { n: 2 } }),
|
|
||||||
$_("photos"),
|
|
||||||
$_("summit-book"),
|
$_("summit-book"),
|
||||||
|
$_("photos"),
|
||||||
...(pb.authStore.model ? [$_("comment", { values: { n: 2 } })] : []),
|
...(pb.authStore.model ? [$_("comment", { values: { n: 2 } })] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -77,6 +76,8 @@
|
|||||||
let commentCreateLoading: boolean = $state(false);
|
let commentCreateLoading: boolean = $state(false);
|
||||||
let commentDeleteLoading: boolean = false;
|
let commentDeleteLoading: boolean = false;
|
||||||
|
|
||||||
|
let fullDescription: boolean = $state(false);
|
||||||
|
|
||||||
onMount(async () => {});
|
onMount(async () => {});
|
||||||
|
|
||||||
function openMarkerPopup(i: number) {
|
function openMarkerPopup(i: number) {
|
||||||
@@ -160,7 +161,7 @@
|
|||||||
<div
|
<div
|
||||||
class="trail-info-panel mx-auto {mode == 'list'
|
class="trail-info-panel mx-auto {mode == 'list'
|
||||||
? ''
|
? ''
|
||||||
: 'border border-input-border rounded-3xl'} h-full"
|
: 'border border-input-border rounded-3xl'} h-full overflow-y-scroll"
|
||||||
style="max-width: min(100%, 76rem);"
|
style="max-width: min(100%, 76rem);"
|
||||||
>
|
>
|
||||||
<div class="trail-info-panel-header">
|
<div class="trail-info-panel-header">
|
||||||
@@ -176,7 +177,7 @@
|
|||||||
></div>
|
></div>
|
||||||
{#if (trail.public || trailIsShared) && pb.authStore.model}
|
{#if (trail.public || trailIsShared) && pb.authStore.model}
|
||||||
<div
|
<div
|
||||||
class="flex absolute top-8 right-6 {trail.public &&
|
class="flex absolute top-8 left-8 {trail.public &&
|
||||||
trailIsShared
|
trailIsShared
|
||||||
? 'w-16'
|
? 'w-16'
|
||||||
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
|
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
|
||||||
@@ -256,44 +257,72 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{#if mode == "overview"}
|
<section
|
||||||
<section
|
class="grid grid-cols-2 sm:grid-cols-5 gap-y-4 py-4 border-b border-input-border px-3"
|
||||||
class="grid grid-cols-2 sm:grid-cols-5 gap-y-4 justify-around py-8 border-b border-input-border"
|
>
|
||||||
>
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="font-medium"
|
||||||
|
>{#if mode == "overview"}
|
||||||
|
{$_("distance")}
|
||||||
|
{:else}
|
||||||
|
<i class="fa fa-left-right"></i>
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
|
<span class=""
|
||||||
|
>{formatDistance(trail.distance)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="font-medium"
|
||||||
|
>{#if mode == "overview"}
|
||||||
|
{$_("est-duration")}
|
||||||
|
{:else}
|
||||||
|
<i class="fa fa-clock"></i>
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
|
<span class=""
|
||||||
|
>{formatTimeHHMM(trail.duration)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="font-medium"
|
||||||
|
>{#if mode == "overview"}
|
||||||
|
{$_("elevation-gain")}
|
||||||
|
{:else}
|
||||||
|
<i class="fa fa-arrow-trend-up"></i>
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
|
<span class=""
|
||||||
|
>{formatElevation(trail.elevation_gain)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="font-medium"
|
||||||
|
>{#if mode == "overview"}
|
||||||
|
{$_("elevation-loss")}
|
||||||
|
{:else}
|
||||||
|
<i class="fa fa-arrow-trend-down"></i>
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
|
<span class=""
|
||||||
|
>{formatElevation(trail.elevation_loss)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#if trail.expand?.category}
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span>{$_("distance")}</span>
|
<span class="font-medium"
|
||||||
<span class="font-semibold text-lg"
|
>{#if mode == "overview"}
|
||||||
>{formatDistance(trail.distance)}</span
|
{$_("category")}
|
||||||
|
{:else}
|
||||||
|
<i class="fa fa-route"></i>
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
|
<span class=""
|
||||||
|
>{$_(trail.expand.category.name)}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
{/if}
|
||||||
<span>{$_("est-duration")}</span>
|
</section>
|
||||||
<span class="font-semibold text-lg"
|
|
||||||
>{formatTimeHHMM(trail.duration)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span>{$_("elevation-gain")}</span>
|
|
||||||
<span class="font-semibold text-lg"
|
|
||||||
>{formatElevation(trail.elevation_gain)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span>{$_("elevation-loss")}</span>
|
|
||||||
<span class="font-semibold text-lg"
|
|
||||||
>{formatElevation(trail.elevation_loss)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{#if trail.expand?.category}
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span>{$_("category")}</span>
|
|
||||||
<span class="font-semibold text-lg"
|
|
||||||
>{$_(trail.expand.category.name)}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
{#if trail.tags && trail.tags.length > 0}
|
{#if trail.tags && trail.tags.length > 0}
|
||||||
<hr class="border-separator" />
|
<hr class="border-separator" />
|
||||||
<section class="flex p-8 gap-4 text-gray-600">
|
<section class="flex p-8 gap-4 text-gray-600">
|
||||||
@@ -304,138 +333,151 @@
|
|||||||
<hr class="border-separator" />
|
<hr class="border-separator" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<section class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0">
|
|
||||||
<Tabs {tabs} bind:activeTab></Tabs>
|
|
||||||
</section>
|
|
||||||
<section class="trail-info-panel-content px-8">
|
<section class="trail-info-panel-content px-8">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 my-4 gap-8"
|
class="grid grid-cols-1 my-4 gap-8"
|
||||||
class:md:grid-cols-[1fr_18rem]={mode == "overview"}
|
class:md:grid-cols-[1fr_18rem]={mode == "overview"}
|
||||||
>
|
>
|
||||||
{#if activeTab == 0}
|
<div>
|
||||||
{#if trail.description?.length}
|
{#if trail.description?.length}
|
||||||
|
<h4 class="text-2xl font-semibold my-4">
|
||||||
|
{$_("description")}
|
||||||
|
</h4>
|
||||||
|
|
||||||
<article class="text-justify whitespace-pre-line text-sm">
|
<article class="text-justify whitespace-pre-line text-sm">
|
||||||
{trail.description}
|
{!fullDescription
|
||||||
|
? trail.description?.substring(0, 300)
|
||||||
|
: trail.description}
|
||||||
|
{#if (trail.description?.length ?? 0) > 300 && !fullDescription}
|
||||||
|
<button
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
fullDescription = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
... <span class="underline"
|
||||||
|
>{$_("read-more")}</span
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</article>
|
</article>
|
||||||
{:else}
|
{:else}
|
||||||
<EmptyStateDescription></EmptyStateDescription>
|
<EmptyStateDescription></EmptyStateDescription>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
<h4 class="text-2xl font-semibold mb-6 mt-12">{$_('route')}</h4>
|
||||||
{#if activeTab == 1}
|
<TrailTimeline
|
||||||
{#if trail.expand?.waypoints.length}
|
{trail}
|
||||||
<ul>
|
onmouseenter={openMarkerPopup}
|
||||||
{#each trail.expand?.waypoints ?? [] as waypoint, i}
|
onmouseleave={closeMarkerPopup}
|
||||||
<li
|
></TrailTimeline>
|
||||||
onmouseenter={() => openMarkerPopup(i)}
|
|
||||||
onmouseleave={() => closeMarkerPopup(i)}
|
<div class="mb-6 mt-12">
|
||||||
>
|
<Tabs {tabs} bind:activeTab></Tabs>
|
||||||
<WaypointCard {waypoint}></WaypointCard>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{:else}
|
|
||||||
<EmptyStateWaypoint></EmptyStateWaypoint>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#if activeTab == 2}
|
|
||||||
{#if trail.photos.length}
|
|
||||||
<div
|
|
||||||
id="photo-gallery"
|
|
||||||
class="grid grid-cols-1 {mode == 'overview'
|
|
||||||
? 'sm:grid-cols-2 md:grid-cols-3'
|
|
||||||
: ''} gap-4"
|
|
||||||
>
|
|
||||||
<PhotoGallery
|
|
||||||
photos={trail.photos.map((p) =>
|
|
||||||
getFileURL(trail, p),
|
|
||||||
)}
|
|
||||||
bind:this={gallery}
|
|
||||||
></PhotoGallery>
|
|
||||||
{#each trail.photos ?? [] as photo, i}
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
||||||
<img
|
|
||||||
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
|
||||||
onclick={() => gallery.openGallery(i)}
|
|
||||||
src={getFileURL(trail, photo)}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<EmptyStatePhotos></EmptyStatePhotos>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#if activeTab == 3}
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<SummitLogTable
|
|
||||||
summitLogs={trail.expand?.summit_logs}
|
|
||||||
showAuthor
|
|
||||||
showRoute
|
|
||||||
showPhotos
|
|
||||||
></SummitLogTable>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{#if activeTab == 0}
|
||||||
{#if activeTab == 4}
|
<div class="overflow-x-auto">
|
||||||
<div>
|
<SummitLogTable
|
||||||
{#if $currentUser}
|
summitLogs={trail.expand?.summit_logs}
|
||||||
<div class="flex items-center gap-4">
|
showAuthor
|
||||||
<img
|
showRoute
|
||||||
class="rounded-full w-10 aspect-square"
|
showPhotos
|
||||||
src={getFileURL(
|
></SummitLogTable>
|
||||||
$currentUser,
|
</div>
|
||||||
$currentUser.avatar,
|
{/if}
|
||||||
) ||
|
{#if activeTab == 1}
|
||||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
{#if trail.photos.length}
|
||||||
alt="avatar"
|
<div
|
||||||
/>
|
id="photo-gallery"
|
||||||
<div class="basis-full">
|
class="grid grid-cols-1 {mode == 'overview'
|
||||||
<Textarea
|
? 'sm:grid-cols-2 md:grid-cols-3'
|
||||||
bind:value={newComment.text}
|
: ''} gap-4"
|
||||||
rows={2}
|
>
|
||||||
placeholder="Add comment..."
|
<PhotoGallery
|
||||||
></Textarea>
|
photos={trail.photos.map((p) =>
|
||||||
</div>
|
getFileURL(trail, p),
|
||||||
</div>
|
)}
|
||||||
<div class="flex justify-end mt-3">
|
bind:this={gallery}
|
||||||
<Button
|
></PhotoGallery>
|
||||||
onclick={createComment}
|
{#each trail.photos ?? [] as photo, i}
|
||||||
loading={commentCreateLoading}
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
secondary={true}
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
disabled={commentCreateLoading ||
|
<img
|
||||||
newComment.text.length == 0}>Comment</Button
|
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
||||||
>
|
onclick={() => gallery.openGallery(i)}
|
||||||
</div>
|
src={getFileURL(trail, photo)}
|
||||||
{/if}
|
alt=""
|
||||||
{#if commentsLoading}
|
/>
|
||||||
{#each { length: 3 } as _, index}
|
{/each}
|
||||||
<SkeletonNotificationCard
|
|
||||||
></SkeletonNotificationCard>
|
|
||||||
{/each}
|
|
||||||
{:else if $comments.length == 0}
|
|
||||||
<div class="my-4">
|
|
||||||
<EmptyStateComment></EmptyStateComment>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="space-y-4">
|
<EmptyStatePhotos></EmptyStatePhotos>
|
||||||
{#each $comments ?? [] as comment}
|
|
||||||
<li>
|
|
||||||
<CommentCard
|
|
||||||
{comment}
|
|
||||||
mode={comment.author == $currentUser?.id
|
|
||||||
? "edit"
|
|
||||||
: "show"}
|
|
||||||
ondelete={deleteComment}
|
|
||||||
onedit={editComment}
|
|
||||||
></CommentCard>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{#if activeTab == 2}
|
||||||
|
<div>
|
||||||
|
{#if $currentUser}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
class="rounded-full w-10 aspect-square"
|
||||||
|
src={getFileURL(
|
||||||
|
$currentUser,
|
||||||
|
$currentUser.avatar,
|
||||||
|
) ||
|
||||||
|
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||||
|
alt="avatar"
|
||||||
|
/>
|
||||||
|
<div class="basis-full">
|
||||||
|
<Textarea
|
||||||
|
bind:value={newComment.text}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Add comment..."
|
||||||
|
></Textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-3">
|
||||||
|
<Button
|
||||||
|
onclick={createComment}
|
||||||
|
loading={commentCreateLoading}
|
||||||
|
secondary={true}
|
||||||
|
disabled={commentCreateLoading ||
|
||||||
|
newComment.text.length == 0}
|
||||||
|
>Comment</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if commentsLoading}
|
||||||
|
{#each { length: 3 } as _, index}
|
||||||
|
<SkeletonNotificationCard
|
||||||
|
></SkeletonNotificationCard>
|
||||||
|
{/each}
|
||||||
|
{:else if $comments.length == 0}
|
||||||
|
<div class="my-4">
|
||||||
|
<EmptyStateComment></EmptyStateComment>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="space-y-4">
|
||||||
|
{#each $comments ?? [] as comment}
|
||||||
|
<li>
|
||||||
|
<CommentCard
|
||||||
|
{comment}
|
||||||
|
mode={comment.author ==
|
||||||
|
$currentUser?.id
|
||||||
|
? "edit"
|
||||||
|
: "show"}
|
||||||
|
ondelete={deleteComment}
|
||||||
|
onedit={editComment}
|
||||||
|
></CommentCard>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if mode == "overview"}
|
{#if mode == "overview"}
|
||||||
<div class="relative h-72 rounded-xl overflow-hidden">
|
<div class="sticky top-4 h-72 rounded-xl overflow-hidden">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[trail]}
|
trails={[trail]}
|
||||||
activeTrail={0}
|
activeTrail={0}
|
||||||
|
|||||||
101
web/src/lib/components/trail/trail_timeline.svelte
Normal file
101
web/src/lib/components/trail/trail_timeline.svelte
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Trail } from "$lib/models/trail";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
import { formatDistance } from "$lib/util/format_util";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import PhotoGallery from "../photo_gallery.svelte";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
trail: Trail;
|
||||||
|
onmouseenter?: (index: number) => void;
|
||||||
|
onmouseleave?: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let gallery: PhotoGallery;
|
||||||
|
|
||||||
|
let { trail, onmouseenter, onmouseleave }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="trail-timeline relative grid grid-cols-[auto_1fr] items-start gap-x-4 gap-y-6"
|
||||||
|
>
|
||||||
|
<div class="bg-background">
|
||||||
|
<i class="fa fa-bullseye"></i>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<p class="font-semibold">{$_("start")}</p>
|
||||||
|
</div>
|
||||||
|
{#each trail.expand?.waypoints ?? [] as wp, i}
|
||||||
|
<div
|
||||||
|
class="bg-background cursor-pointer"
|
||||||
|
role="presentation"
|
||||||
|
onmouseenter={() => onmouseenter?.(i)}
|
||||||
|
onmouseleave={() => onmouseleave?.(i)}
|
||||||
|
>
|
||||||
|
<i class="fa fa-{wp.icon ?? 'circle'}"></i>
|
||||||
|
{#if wp.distance_from_start}
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{formatDistance(wp.distance_from_start)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-input-border rounded-xl overflow-hidden">
|
||||||
|
<PhotoGallery
|
||||||
|
photos={wp.photos.map((p) => getFileURL(wp, p))}
|
||||||
|
bind:this={gallery}
|
||||||
|
></PhotoGallery>
|
||||||
|
{#if wp.photos.length}
|
||||||
|
<div
|
||||||
|
class="grid {wp.photos.length > 1
|
||||||
|
? 'grid-cols-[8fr_5fr]'
|
||||||
|
: 'grid-cols-1'} cursor-pointer"
|
||||||
|
role="presentation"
|
||||||
|
onclick={() => gallery.openGallery()}
|
||||||
|
>
|
||||||
|
{#each wp.photos as photo, i}
|
||||||
|
<img
|
||||||
|
class="object-cover h-full max-h-80 w-full"
|
||||||
|
class:row-span-2={i == 0 && wp.photos.length > 2}
|
||||||
|
src={getFileURL(wp, photo)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="p-4">
|
||||||
|
<h5 class="text-xl font-semibold">{wp.name}</h5>
|
||||||
|
<span class="text-sm text-gray-500"
|
||||||
|
>{wp.lat.toFixed(5)}, {wp.lon.toFixed(5)}</span
|
||||||
|
>
|
||||||
|
<p class="whitespace-pre-line">
|
||||||
|
{wp.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="bg-background">
|
||||||
|
<i class="fa fa-flag-checkered"></i>
|
||||||
|
{#if trail.distance}
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{formatDistance(trail.distance)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<p class="font-semibold">{$_("finish")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.trail-timeline::before {
|
||||||
|
border-right: 2px dotted rgba(var(--input-border));
|
||||||
|
bottom: 0;
|
||||||
|
content: "";
|
||||||
|
left: 7.2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 1px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "Datei {file} ist zu groß (max. {size})",
|
"file-too-big": "Datei {file} ist zu groß (max. {size})",
|
||||||
"filter-categories": "Kategorien filtern",
|
"filter-categories": "Kategorien filtern",
|
||||||
"filter-difficulty": "Schwierigkeit filtern",
|
"filter-difficulty": "Schwierigkeit filtern",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Karte fokussieren auf",
|
"focus-map-on": "Karte fokussieren auf",
|
||||||
"follow": "Folgen",
|
"follow": "Folgen",
|
||||||
"followers": "Follower",
|
"followers": "Follower",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Route entfernt aus",
|
"removed-trail-from": "Route entfernt aus",
|
||||||
"required": "Pflichtfeld",
|
"required": "Pflichtfeld",
|
||||||
"reset-password": "Passwort zurücksetzen",
|
"reset-password": "Passwort zurücksetzen",
|
||||||
|
"route": "",
|
||||||
"route-point": "Routenpunkt",
|
"route-point": "Routenpunkt",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"save-list": "Liste speichern",
|
"save-list": "Liste speichern",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Sortieren",
|
"sort": "Sortieren",
|
||||||
"spanish": "Spanisch",
|
"spanish": "Spanisch",
|
||||||
"speed": "Geschwindigkeit",
|
"speed": "Geschwindigkeit",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistiken",
|
"statistics": "Statistiken",
|
||||||
"stop-drawing": "Zeichnen beenden",
|
"stop-drawing": "Zeichnen beenden",
|
||||||
"stop-editing": "Bearbeiten beenden",
|
"stop-editing": "Bearbeiten beenden",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"finish": "Finish",
|
||||||
"focus-map-on": "Focus map on",
|
"focus-map-on": "Focus map on",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Removed trail from",
|
"removed-trail-from": "Removed trail from",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"reset-password": "Reset Password",
|
"reset-password": "Reset Password",
|
||||||
|
"route": "Route",
|
||||||
"route-point": "Route Point",
|
"route-point": "Route Point",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save-list": "Save List",
|
"save-list": "Save List",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"spanish": "Spanish",
|
"spanish": "Spanish",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
|
"start": "Start",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Stop drawing",
|
"stop-drawing": "Stop drawing",
|
||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "Archivo {file} es demasiado grande (max. {size})",
|
"file-too-big": "Archivo {file} es demasiado grande (max. {size})",
|
||||||
"filter-categories": "Filtrar categorías",
|
"filter-categories": "Filtrar categorías",
|
||||||
"filter-difficulty": "Filtrar dificultad",
|
"filter-difficulty": "Filtrar dificultad",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Centrar mapa sobre",
|
"focus-map-on": "Centrar mapa sobre",
|
||||||
"follow": "Seguir",
|
"follow": "Seguir",
|
||||||
"followers": "Seguidores",
|
"followers": "Seguidores",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Ruta borrada de",
|
"removed-trail-from": "Ruta borrada de",
|
||||||
"required": "Obligatorio",
|
"required": "Obligatorio",
|
||||||
"reset-password": "Restablecer Contraseña",
|
"reset-password": "Restablecer Contraseña",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"save-list": "Guardar Lista",
|
"save-list": "Guardar Lista",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Ordenar",
|
"sort": "Ordenar",
|
||||||
"spanish": "Español",
|
"spanish": "Español",
|
||||||
"speed": "Velocidad",
|
"speed": "Velocidad",
|
||||||
|
"start": "",
|
||||||
"statistics": "Estadísticas",
|
"statistics": "Estadísticas",
|
||||||
"stop-drawing": "Parar de diseñar",
|
"stop-drawing": "Parar de diseñar",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Centrer la carte sur",
|
"focus-map-on": "Centrer la carte sur",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Enlever l'itinéraire de",
|
"removed-trail-from": "Enlever l'itinéraire de",
|
||||||
"required": "Requis",
|
"required": "Requis",
|
||||||
"reset-password": "Réinitialiser le mot de passe",
|
"reset-password": "Réinitialiser le mot de passe",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Sauvegarder",
|
"save": "Sauvegarder",
|
||||||
"save-list": "Sauvegarder la liste",
|
"save-list": "Sauvegarder la liste",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Trier",
|
"sort": "Trier",
|
||||||
"spanish": "Espagnol",
|
"spanish": "Espagnol",
|
||||||
"speed": "Vitesse",
|
"speed": "Vitesse",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Arrêter de tracer",
|
"stop-drawing": "Arrêter de tracer",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Focus map on",
|
"focus-map-on": "Focus map on",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Eltávolított nyomvonal a",
|
"removed-trail-from": "Eltávolított nyomvonal a",
|
||||||
"required": "Kötelező",
|
"required": "Kötelező",
|
||||||
"reset-password": "Reset Password",
|
"reset-password": "Reset Password",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Mentés",
|
"save": "Mentés",
|
||||||
"save-list": "Save List",
|
"save-list": "Save List",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Rendezés",
|
"sort": "Rendezés",
|
||||||
"spanish": "spanyol",
|
"spanish": "spanyol",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Stop drawing",
|
"stop-drawing": "Stop drawing",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} è troppo grande (max. {size})",
|
"file-too-big": "File {file} è troppo grande (max. {size})",
|
||||||
"filter-categories": "Filtrare categorie",
|
"filter-categories": "Filtrare categorie",
|
||||||
"filter-difficulty": "Filtrare difficoltà",
|
"filter-difficulty": "Filtrare difficoltà",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Focus sulla mappa",
|
"focus-map-on": "Focus sulla mappa",
|
||||||
"follow": "Seguire",
|
"follow": "Seguire",
|
||||||
"followers": "Seguaci",
|
"followers": "Seguaci",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Percorso rimosso da",
|
"removed-trail-from": "Percorso rimosso da",
|
||||||
"required": "Obbligatorio",
|
"required": "Obbligatorio",
|
||||||
"reset-password": "Ripristinare Password",
|
"reset-password": "Ripristinare Password",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Salva",
|
"save": "Salva",
|
||||||
"save-list": "Salta Lista",
|
"save-list": "Salta Lista",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Ordina",
|
"sort": "Ordina",
|
||||||
"spanish": "spagnolo",
|
"spanish": "spagnolo",
|
||||||
"speed": "Velocità",
|
"speed": "Velocità",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistiche",
|
"statistics": "Statistiche",
|
||||||
"stop-drawing": "Smettere di disegnare",
|
"stop-drawing": "Smettere di disegnare",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Focus map on",
|
"focus-map-on": "Focus map on",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "De wandelroute is verwijderd van",
|
"removed-trail-from": "De wandelroute is verwijderd van",
|
||||||
"required": "Verplicht",
|
"required": "Verplicht",
|
||||||
"reset-password": "Reset Password",
|
"reset-password": "Reset Password",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Bewaren",
|
"save": "Bewaren",
|
||||||
"save-list": "Save List",
|
"save-list": "Save List",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Sorteren",
|
"sort": "Sorteren",
|
||||||
"spanish": "Spaans",
|
"spanish": "Spaans",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Stop drawing",
|
"stop-drawing": "Stop drawing",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "Plik {file} jest za duży (maks. {size})",
|
"file-too-big": "Plik {file} jest za duży (maks. {size})",
|
||||||
"filter-categories": "Filtruj kategorie",
|
"filter-categories": "Filtruj kategorie",
|
||||||
"filter-difficulty": "Filtruj poziom trudności",
|
"filter-difficulty": "Filtruj poziom trudności",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Skoncentruj mapę na",
|
"focus-map-on": "Skoncentruj mapę na",
|
||||||
"follow": "Obserwuj",
|
"follow": "Obserwuj",
|
||||||
"followers": "Obserwujący",
|
"followers": "Obserwujący",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Usunięto szlak z",
|
"removed-trail-from": "Usunięto szlak z",
|
||||||
"required": "Wymagane",
|
"required": "Wymagane",
|
||||||
"reset-password": "Resetuj hasło",
|
"reset-password": "Resetuj hasło",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Zapisz",
|
"save": "Zapisz",
|
||||||
"save-list": "Zapisz listę",
|
"save-list": "Zapisz listę",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Sortowanie",
|
"sort": "Sortowanie",
|
||||||
"spanish": "hiszpański",
|
"spanish": "hiszpański",
|
||||||
"speed": "Prędkość",
|
"speed": "Prędkość",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statystyki",
|
"statistics": "Statystyki",
|
||||||
"stop-drawing": "Przestań rysować",
|
"stop-drawing": "Przestań rysować",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filtrar categorias",
|
"filter-categories": "Filtrar categorias",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "Centrar mapa em",
|
"focus-map-on": "Centrar mapa em",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "Trilha removida de",
|
"removed-trail-from": "Trilha removida de",
|
||||||
"required": "Obrigatório",
|
"required": "Obrigatório",
|
||||||
"reset-password": "Reset Password",
|
"reset-password": "Reset Password",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"save-list": "Gravar lista",
|
"save-list": "Gravar lista",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "Ordenar",
|
"sort": "Ordenar",
|
||||||
"spanish": "Espanhol",
|
"spanish": "Espanhol",
|
||||||
"speed": "Velocidade",
|
"speed": "Velocidade",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "Parar desenho",
|
"stop-drawing": "Parar desenho",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "筛选分类",
|
"filter-categories": "筛选分类",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"finish": "",
|
||||||
"focus-map-on": "地图聚焦于",
|
"focus-map-on": "地图聚焦于",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"followers": "Followers",
|
"followers": "Followers",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"removed-trail-from": "路线已删除自",
|
"removed-trail-from": "路线已删除自",
|
||||||
"required": "必填",
|
"required": "必填",
|
||||||
"reset-password": "重置密码",
|
"reset-password": "重置密码",
|
||||||
|
"route": "",
|
||||||
"route-point": "",
|
"route-point": "",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"save-list": "保存列表",
|
"save-list": "保存列表",
|
||||||
@@ -261,6 +263,7 @@
|
|||||||
"sort": "排序",
|
"sort": "排序",
|
||||||
"spanish": "西班牙语",
|
"spanish": "西班牙语",
|
||||||
"speed": "速度",
|
"speed": "速度",
|
||||||
|
"start": "",
|
||||||
"statistics": "Statistics",
|
"statistics": "Statistics",
|
||||||
"stop-drawing": "停止绘制",
|
"stop-drawing": "停止绘制",
|
||||||
"stop-editing": "",
|
"stop-editing": "",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z, ZodType } from "zod";
|
import { z, ZodType } from "zod";
|
||||||
import type { Waypoint } from "../waypoint";
|
import type { Waypoint } from "../waypoint";
|
||||||
|
import { icons } from "$lib/util/icon_util";
|
||||||
|
|
||||||
const WaypointCreateSchema = z.object({
|
const WaypointCreateSchema = z.object({
|
||||||
id: z.string().length(15).optional(),
|
id: z.string().length(15).optional(),
|
||||||
@@ -7,7 +8,8 @@ const WaypointCreateSchema = z.object({
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
lat: z.number({coerce: true}).min(-90).max(90),
|
lat: z.number({coerce: true}).min(-90).max(90),
|
||||||
lon: z.number({coerce: true}).min(-180).max(180),
|
lon: z.number({coerce: true}).min(-180).max(180),
|
||||||
icon: z.string().optional(),
|
distance_from_start: z.number({coerce: true}).min(0).optional(),
|
||||||
|
icon: z.enum(icons).optional(),
|
||||||
author: z.string().length(15),
|
author: z.string().length(15),
|
||||||
photos: z.array(z.string()).default([]),
|
photos: z.array(z.string()).default([]),
|
||||||
}) satisfies ZodType<Partial<Waypoint>>
|
}) satisfies ZodType<Partial<Waypoint>>
|
||||||
@@ -17,7 +19,8 @@ const WaypointUpdateSchema = z.object({
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
lat: z.number({coerce: true}).min(-90).max(90).optional(),
|
lat: z.number({coerce: true}).min(-90).max(90).optional(),
|
||||||
lon: z.number({coerce: true}).min(-180).max(180).optional(),
|
lon: z.number({coerce: true}).min(-180).max(180).optional(),
|
||||||
icon: z.string().default("circle").optional(),
|
distance_from_start: z.number({coerce: true}).min(0).optional(),
|
||||||
|
icon: z.enum(icons).default("circle").optional(),
|
||||||
photos: z.array(z.string()).optional(),
|
photos: z.array(z.string()).optional(),
|
||||||
"photos-": z.string().optional(),
|
"photos-": z.string().optional(),
|
||||||
"photos+": z.string().optional(),
|
"photos+": z.string().optional(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { icons } from "$lib/util/icon_util";
|
||||||
import * as M from "maplibre-gl";
|
import * as M from "maplibre-gl";
|
||||||
|
|
||||||
class Waypoint {
|
class Waypoint {
|
||||||
@@ -6,14 +7,15 @@ class Waypoint {
|
|||||||
description?: string;
|
description?: string;
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lon: number;
|
||||||
icon?: string;
|
distance_from_start?: number;
|
||||||
|
icon?: typeof icons[number];
|
||||||
marker?: M.Marker;
|
marker?: M.Marker;
|
||||||
photos: string[];
|
photos: string[];
|
||||||
_photos?: File[];
|
_photos?: File[];
|
||||||
author: string;
|
author: string;
|
||||||
|
|
||||||
constructor(lat: number, lon: number, params?: {
|
constructor(lat: number, lon: number, params?: {
|
||||||
id?: string, name?: string, description?: string, icon?: string, marker?: M.Marker, photos?: string[];
|
id?: string, name?: string, description?: string, icon?: typeof icons[number], marker?: M.Marker, photos?: string[];
|
||||||
}) {
|
}) {
|
||||||
this.id = params?.id;
|
this.id = params?.id;
|
||||||
this.name = params?.name ?? "";
|
this.name = params?.name ?? "";
|
||||||
|
|||||||
@@ -38,10 +38,11 @@ export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker
|
|||||||
})
|
})
|
||||||
|
|
||||||
const content = document.createElement("div");
|
const content = document.createElement("div");
|
||||||
|
content.className = "p-2"
|
||||||
|
|
||||||
const spanElement = document.createElement("span");
|
const spanElement = document.createElement("span");
|
||||||
const iconElement = document.createElement("i");
|
const iconElement = document.createElement("i");
|
||||||
const iconName = icons.includes(waypoint.icon ?? "") ? waypoint.icon : "circle";
|
const iconName = waypoint.icon && icons.includes(waypoint.icon) ? waypoint.icon : "circle";
|
||||||
iconElement.classList.add("fa", `fa-${iconName}`)
|
iconElement.classList.add("fa", `fa-${iconName}`)
|
||||||
spanElement.appendChild(iconElement);
|
spanElement.appendChild(iconElement);
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ export function createAnchorMarker(lat: number, lon: number, index: number,
|
|||||||
const popup = new M.Popup()
|
const popup = new M.Popup()
|
||||||
|
|
||||||
const popupContent = document.createElement("div");
|
const popupContent = document.createElement("div");
|
||||||
|
popupContent.className = "py-3 pl-3"
|
||||||
const anchorH = document.createElement("h5")
|
const anchorH = document.createElement("h5")
|
||||||
anchorH.classList.add("text-base", "font-medium");
|
anchorH.classList.add("text-base", "font-medium");
|
||||||
anchorH.textContent = get(_)("route-point") + " #" + index;
|
anchorH.textContent = get(_)("route-point") + " #" + index;
|
||||||
@@ -140,7 +142,7 @@ export function createPopupFromTrail(trail: Trail) {
|
|||||||
: get(theme) === "light"
|
: get(theme) === "light"
|
||||||
? emptyStateTrailLight
|
? emptyStateTrailLight
|
||||||
: emptyStateTrailDark;
|
: emptyStateTrailDark;
|
||||||
const popup = new M.Popup({ maxWidth: "320px" });
|
const popup = new M.Popup({ maxWidth: "420px" });
|
||||||
// Create a container element for the popup content
|
// Create a container element for the popup content
|
||||||
const linkElement = document.createElement("a");
|
const linkElement = document.createElement("a");
|
||||||
linkElement.href = `/map/trail/${trail.id}`; // Set href safely
|
linkElement.href = `/map/trail/${trail.id}`; // Set href safely
|
||||||
@@ -148,7 +150,7 @@ export function createPopupFromTrail(trail: Trail) {
|
|||||||
|
|
||||||
// Create a list item element
|
// Create a list item element
|
||||||
const listItem = document.createElement("li");
|
const listItem = document.createElement("li");
|
||||||
listItem.className = "flex items-center gap-4 cursor-pointer text-content max-w-80";
|
listItem.className = "flex gap-4 cursor-pointer text-content max-w-80";
|
||||||
|
|
||||||
// Create the image container
|
// Create the image container
|
||||||
const imageContainer = document.createElement("div");
|
const imageContainer = document.createElement("div");
|
||||||
@@ -156,13 +158,14 @@ export function createPopupFromTrail(trail: Trail) {
|
|||||||
|
|
||||||
// Create the image element
|
// Create the image element
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.className = "h-14 w-14 object-cover rounded-xl";
|
img.className = "h-full max-w-20 object-cover";
|
||||||
img.src = thumbnail; // Set image source safely
|
img.src = thumbnail; // Set image source safely
|
||||||
img.alt = ""; // Always include a safe alt attribute
|
img.alt = ""; // Always include a safe alt attribute
|
||||||
imageContainer.appendChild(img);
|
imageContainer.appendChild(img);
|
||||||
|
|
||||||
// Create the text container
|
// Create the text container
|
||||||
const textContainer = document.createElement("div");
|
const textContainer = document.createElement("div");
|
||||||
|
textContainer.className = "py-2"
|
||||||
|
|
||||||
// Add trail name
|
// Add trail name
|
||||||
const trailName = document.createElement("h4");
|
const trailName = document.createElement("h4");
|
||||||
@@ -193,7 +196,7 @@ export function createPopupFromTrail(trail: Trail) {
|
|||||||
// Create the grid container for additional stats
|
// Create the grid container for additional stats
|
||||||
const statsContainer = document.createElement("div");
|
const statsContainer = document.createElement("div");
|
||||||
statsContainer.className =
|
statsContainer.className =
|
||||||
"grid grid-cols-2 mt-2 gap-x-4 gap-y-2 text-sm text-gray-500 flex-wrap";
|
"grid grid-cols-2 mt-2 gap-x-4 text-gray-500 flex-wrap";
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ icon: "fa-left-right", value: formatDistance(trail.distance) },
|
{ icon: "fa-left-right", value: formatDistance(trail.distance) },
|
||||||
|
|||||||
@@ -943,6 +943,7 @@ export class ElevationProfile {
|
|||||||
if (distance < minDistances[waypointIndex]) {
|
if (distance < minDistances[waypointIndex]) {
|
||||||
minDistances[waypointIndex] = distance;
|
minDistances[waypointIndex] = distance;
|
||||||
this.waypointPositions[waypointIndex] = this.cumulatedDistanceAdjustedUnit[i];
|
this.waypointPositions[waypointIndex] = this.cumulatedDistanceAdjustedUnit[i];
|
||||||
|
waypoint.distance_from_start = this.cumulatedDistanceAdjustedUnit[i] * 1000;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
t.expand = {} as any
|
t.expand = {} as any
|
||||||
}
|
}
|
||||||
t.expand!.author = await pb.collection("users_anonymous").getOne(t.author);
|
t.expand!.author = await pb.collection("users_anonymous").getOne(t.author);
|
||||||
|
t.expand?.waypoints?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
|
||||||
}
|
}
|
||||||
return json(r)
|
return json(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export async function GET(event: RequestEvent) {
|
|||||||
log.expand!.author = await pb.collection("users_anonymous").getOne(log.author);
|
log.expand!.author = await pb.collection("users_anonymous").getOne(log.author);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sort waypoints by distance
|
||||||
|
r.expand?.waypoints?.sort((a, b) => (a.distance_from_start ?? 0) - (b.distance_from_start ?? 0))
|
||||||
return json(r)
|
return json(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw handleError(e)
|
throw handleError(e)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
|
||||||
text: t.name,
|
text: t.name,
|
||||||
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
|
||||||
value: t,
|
value: t.id,
|
||||||
icon: "route",
|
icon: "route",
|
||||||
}));
|
}));
|
||||||
const listItems = r[1].hits.map((t: ListSearchResult) => ({
|
const listItems = r[1].hits.map((t: ListSearchResult) => ({
|
||||||
@@ -93,7 +93,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchClick(item: SearchItem) {
|
function handleSearchClick(item: SearchItem) {
|
||||||
if (item.icon === "layer-group") {
|
if (item.icon == "route") {
|
||||||
|
goto(`/trail/view/${item.value}`);
|
||||||
|
} else if (item.icon == "layer-group") {
|
||||||
goto(`/lists?list=${item.value}`);
|
goto(`/lists?list=${item.value}`);
|
||||||
} else {
|
} else {
|
||||||
map?.setCenter([item.value.lon, item.value.lat]);
|
map?.setCenter([item.value.lon, item.value.lat]);
|
||||||
@@ -220,7 +222,7 @@
|
|||||||
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
|
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
|
||||||
<div
|
<div
|
||||||
id="trail-list"
|
id="trail-list"
|
||||||
class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 px-3 md:px-8"
|
class="flex flex-col items-stretch gap-4 px-3 md:px-8"
|
||||||
>
|
>
|
||||||
<div class="sticky top-0 z-10 bg-background pb-4 space-y-4">
|
<div class="sticky top-0 z-10 bg-background pb-4 space-y-4">
|
||||||
<div class="flex items-center gap-2 md:gap-4">
|
<div class="flex items-center gap-2 md:gap-4">
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
<title>{$trail.name} | {$_("map")} | wanderer</title>
|
<title>{$trail.name} | {$_("map")} | wanderer</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||||
<div class="hidden md:block">
|
<div id="panel" class="hidden md:block">
|
||||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||||
</div>
|
</div>
|
||||||
<div id="trail-details" class="sticky top-[62px]">
|
<div id="trail-details">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
trails={[$trail]}
|
trails={[$trail]}
|
||||||
waypoints={$trail.expand?.waypoints}
|
waypoints={$trail.expand?.waypoints}
|
||||||
@@ -28,11 +28,11 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#trail-details {
|
#trail-details, #panel {
|
||||||
height: calc(100vh);
|
height: calc(100vh);
|
||||||
}
|
}
|
||||||
@media only screen and (min-width: 768px) {
|
@media only screen and (min-width: 768px) {
|
||||||
#trail-details {
|
#trail-details, #panel {
|
||||||
height: calc(100vh - 124px);
|
height: calc(100vh - 124px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user