updates trail panel view

This commit is contained in:
Christian Beutel
2025-01-27 19:38:38 +01:00
parent 5922034183
commit e837b6e04b
25 changed files with 427 additions and 182 deletions

View File

@@ -1,8 +1,6 @@
# v0.14.0
> [!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.
> 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`.
> 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`.
## Maintenance
- Migrates to Svelte 5

View 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)
})
}

View File

@@ -27,15 +27,19 @@
}
.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 {
@apply border-menu-item-background-hover
@apply border-menu-item-background-focus;
}
.tab-active {
@apply font-semibold
}
.tab-active:hover {
@apply bg-transparent
@apply border-transparent;
}
.tooltip {

View File

@@ -40,7 +40,7 @@
<div id="tabs" class="flex gap-2 overflow-x-auto relative {extraClasses}">
<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;"
></div>
{#each tabs as tab, i}

View File

@@ -258,7 +258,7 @@
export function refreshElevationProfile() {
if (activeTrail !== null && data[activeTrail]) {
epc?.setData(data[activeTrail]!, waypoints);
epc?.setData(data[activeTrail]!, waypoints);
}
}
@@ -940,12 +940,12 @@
}
: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) {
top: 4px;
right: 8px;
right: 4px;
line-height: 0;
padding-bottom: 2.5px;
@apply bg-menu-item-background-focus w-3 aspect-square rounded-full;

View File

@@ -43,6 +43,7 @@
import EmptyStateWaypoint from "../empty_states/empty_state_waypoint.svelte";
import EmptyStateDescription from "../empty_states/empty_state_description.svelte";
import { browser } from "$app/environment";
import TrailTimeline from "./trail_timeline.svelte";
interface Props {
trail: Trail;
@@ -54,10 +55,8 @@
let { trail, mode = "map", markers = [], activeTab = 0 }: Props = $props();
const tabs = [
$_("description"),
$_("waypoints", { values: { n: 2 } }),
$_("photos"),
$_("summit-book"),
$_("photos"),
...(pb.authStore.model ? [$_("comment", { values: { n: 2 } })] : []),
];
@@ -77,6 +76,8 @@
let commentCreateLoading: boolean = $state(false);
let commentDeleteLoading: boolean = false;
let fullDescription: boolean = $state(false);
onMount(async () => {});
function openMarkerPopup(i: number) {
@@ -160,7 +161,7 @@
<div
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);"
>
<div class="trail-info-panel-header">
@@ -176,7 +177,7 @@
></div>
{#if (trail.public || trailIsShared) && pb.authStore.model}
<div
class="flex absolute top-8 right-6 {trail.public &&
class="flex absolute top-8 left-8 {trail.public &&
trailIsShared
? 'w-16'
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
@@ -256,44 +257,72 @@
{/if}
</div>
</section>
{#if mode == "overview"}
<section
class="grid grid-cols-2 sm:grid-cols-5 gap-y-4 justify-around py-8 border-b border-input-border"
>
<section
class="grid grid-cols-2 sm:grid-cols-5 gap-y-4 py-4 border-b border-input-border px-3"
>
<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">
<span>{$_("distance")}</span>
<span class="font-semibold text-lg"
>{formatDistance(trail.distance)}</span
<span class="font-medium"
>{#if mode == "overview"}
{$_("category")}
{:else}
<i class="fa fa-route"></i>
{/if}</span
>
<span class=""
>{$_(trail.expand.category.name)}</span
>
</div>
<div class="flex flex-col items-center">
<span>{$_("est-duration")}</span>
<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}
</section>
{#if trail.tags && trail.tags.length > 0}
<hr class="border-separator" />
<section class="flex p-8 gap-4 text-gray-600">
@@ -304,138 +333,151 @@
<hr class="border-separator" />
{/if}
</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">
<div
class="grid grid-cols-1 my-4 gap-8"
class:md:grid-cols-[1fr_18rem]={mode == "overview"}
>
{#if activeTab == 0}
<div>
{#if trail.description?.length}
<h4 class="text-2xl font-semibold my-4">
{$_("description")}
</h4>
<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>
{:else}
<EmptyStateDescription></EmptyStateDescription>
{/if}
{/if}
{#if activeTab == 1}
{#if trail.expand?.waypoints.length}
<ul>
{#each trail.expand?.waypoints ?? [] as waypoint, i}
<li
onmouseenter={() => openMarkerPopup(i)}
onmouseleave={() => closeMarkerPopup(i)}
>
<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>
<h4 class="text-2xl font-semibold mb-6 mt-12">{$_('route')}</h4>
<TrailTimeline
{trail}
onmouseenter={openMarkerPopup}
onmouseleave={closeMarkerPopup}
></TrailTimeline>
<div class="mb-6 mt-12">
<Tabs {tabs} bind:activeTab></Tabs>
</div>
{/if}
{#if activeTab == 4}
<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>
{#if activeTab == 0}
<div class="overflow-x-auto">
<SummitLogTable
summitLogs={trail.expand?.summit_logs}
showAuthor
showRoute
showPhotos
></SummitLogTable>
</div>
{/if}
{#if activeTab == 1}
{#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}
<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>
<EmptyStatePhotos></EmptyStatePhotos>
{/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"}
<div class="relative h-72 rounded-xl overflow-hidden">
<div class="sticky top-4 h-72 rounded-xl overflow-hidden">
<MapWithElevationMaplibre
trails={[trail]}
activeTrail={0}

View 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>

View File

@@ -113,6 +113,7 @@
"file-too-big": "Datei {file} ist zu groß (max. {size})",
"filter-categories": "Kategorien filtern",
"filter-difficulty": "Schwierigkeit filtern",
"finish": "",
"focus-map-on": "Karte fokussieren auf",
"follow": "Folgen",
"followers": "Follower",
@@ -223,6 +224,7 @@
"removed-trail-from": "Route entfernt aus",
"required": "Pflichtfeld",
"reset-password": "Passwort zurücksetzen",
"route": "",
"route-point": "Routenpunkt",
"save": "Speichern",
"save-list": "Liste speichern",
@@ -261,6 +263,7 @@
"sort": "Sortieren",
"spanish": "Spanisch",
"speed": "Geschwindigkeit",
"start": "",
"statistics": "Statistiken",
"stop-drawing": "Zeichnen beenden",
"stop-editing": "Bearbeiten beenden",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "Filter categories",
"filter-difficulty": "Filter difficulty",
"finish": "Finish",
"focus-map-on": "Focus map on",
"follow": "Follow",
"followers": "Followers",
@@ -223,6 +224,7 @@
"removed-trail-from": "Removed trail from",
"required": "Required",
"reset-password": "Reset Password",
"route": "Route",
"route-point": "Route Point",
"save": "Save",
"save-list": "Save List",
@@ -261,6 +263,7 @@
"sort": "Sort",
"spanish": "Spanish",
"speed": "Speed",
"start": "Start",
"statistics": "Statistics",
"stop-drawing": "Stop drawing",
"stop-editing": "Stop editing",

View File

@@ -113,6 +113,7 @@
"file-too-big": "Archivo {file} es demasiado grande (max. {size})",
"filter-categories": "Filtrar categorías",
"filter-difficulty": "Filtrar dificultad",
"finish": "",
"focus-map-on": "Centrar mapa sobre",
"follow": "Seguir",
"followers": "Seguidores",
@@ -223,6 +224,7 @@
"removed-trail-from": "Ruta borrada de",
"required": "Obligatorio",
"reset-password": "Restablecer Contraseña",
"route": "",
"route-point": "",
"save": "Guardar",
"save-list": "Guardar Lista",
@@ -261,6 +263,7 @@
"sort": "Ordenar",
"spanish": "Español",
"speed": "Velocidad",
"start": "",
"statistics": "Estadísticas",
"stop-drawing": "Parar de diseñar",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "Filter categories",
"filter-difficulty": "Filter difficulty",
"finish": "",
"focus-map-on": "Centrer la carte sur",
"follow": "Follow",
"followers": "Followers",
@@ -223,6 +224,7 @@
"removed-trail-from": "Enlever l'itinéraire de",
"required": "Requis",
"reset-password": "Réinitialiser le mot de passe",
"route": "",
"route-point": "",
"save": "Sauvegarder",
"save-list": "Sauvegarder la liste",
@@ -261,6 +263,7 @@
"sort": "Trier",
"spanish": "Espagnol",
"speed": "Vitesse",
"start": "",
"statistics": "Statistics",
"stop-drawing": "Arrêter de tracer",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "Filter categories",
"filter-difficulty": "Filter difficulty",
"finish": "",
"focus-map-on": "Focus map on",
"follow": "Follow",
"followers": "Followers",
@@ -223,6 +224,7 @@
"removed-trail-from": "Eltávolított nyomvonal a",
"required": "Kötelező",
"reset-password": "Reset Password",
"route": "",
"route-point": "",
"save": "Mentés",
"save-list": "Save List",
@@ -261,6 +263,7 @@
"sort": "Rendezés",
"spanish": "spanyol",
"speed": "Speed",
"start": "",
"statistics": "Statistics",
"stop-drawing": "Stop drawing",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} è troppo grande (max. {size})",
"filter-categories": "Filtrare categorie",
"filter-difficulty": "Filtrare difficoltà",
"finish": "",
"focus-map-on": "Focus sulla mappa",
"follow": "Seguire",
"followers": "Seguaci",
@@ -223,6 +224,7 @@
"removed-trail-from": "Percorso rimosso da",
"required": "Obbligatorio",
"reset-password": "Ripristinare Password",
"route": "",
"route-point": "",
"save": "Salva",
"save-list": "Salta Lista",
@@ -261,6 +263,7 @@
"sort": "Ordina",
"spanish": "spagnolo",
"speed": "Velocità",
"start": "",
"statistics": "Statistiche",
"stop-drawing": "Smettere di disegnare",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "Filter categories",
"filter-difficulty": "Filter difficulty",
"finish": "",
"focus-map-on": "Focus map on",
"follow": "Follow",
"followers": "Followers",
@@ -223,6 +224,7 @@
"removed-trail-from": "De wandelroute is verwijderd van",
"required": "Verplicht",
"reset-password": "Reset Password",
"route": "",
"route-point": "",
"save": "Bewaren",
"save-list": "Save List",
@@ -261,6 +263,7 @@
"sort": "Sorteren",
"spanish": "Spaans",
"speed": "Speed",
"start": "",
"statistics": "Statistics",
"stop-drawing": "Stop drawing",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "Plik {file} jest za duży (maks. {size})",
"filter-categories": "Filtruj kategorie",
"filter-difficulty": "Filtruj poziom trudności",
"finish": "",
"focus-map-on": "Skoncentruj mapę na",
"follow": "Obserwuj",
"followers": "Obserwujący",
@@ -223,6 +224,7 @@
"removed-trail-from": "Usunięto szlak z",
"required": "Wymagane",
"reset-password": "Resetuj hasło",
"route": "",
"route-point": "",
"save": "Zapisz",
"save-list": "Zapisz listę",
@@ -261,6 +263,7 @@
"sort": "Sortowanie",
"spanish": "hiszpański",
"speed": "Prędkość",
"start": "",
"statistics": "Statystyki",
"stop-drawing": "Przestań rysować",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "Filtrar categorias",
"filter-difficulty": "Filter difficulty",
"finish": "",
"focus-map-on": "Centrar mapa em",
"follow": "Follow",
"followers": "Followers",
@@ -223,6 +224,7 @@
"removed-trail-from": "Trilha removida de",
"required": "Obrigatório",
"reset-password": "Reset Password",
"route": "",
"route-point": "",
"save": "Guardar",
"save-list": "Gravar lista",
@@ -261,6 +263,7 @@
"sort": "Ordenar",
"spanish": "Espanhol",
"speed": "Velocidade",
"start": "",
"statistics": "Statistics",
"stop-drawing": "Parar desenho",
"stop-editing": "",

View File

@@ -113,6 +113,7 @@
"file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "筛选分类",
"filter-difficulty": "Filter difficulty",
"finish": "",
"focus-map-on": "地图聚焦于",
"follow": "Follow",
"followers": "Followers",
@@ -223,6 +224,7 @@
"removed-trail-from": "路线已删除自",
"required": "必填",
"reset-password": "重置密码",
"route": "",
"route-point": "",
"save": "保存",
"save-list": "保存列表",
@@ -261,6 +263,7 @@
"sort": "排序",
"spanish": "西班牙语",
"speed": "速度",
"start": "",
"statistics": "Statistics",
"stop-drawing": "停止绘制",
"stop-editing": "",

View File

@@ -1,5 +1,6 @@
import { z, ZodType } from "zod";
import type { Waypoint } from "../waypoint";
import { icons } from "$lib/util/icon_util";
const WaypointCreateSchema = z.object({
id: z.string().length(15).optional(),
@@ -7,7 +8,8 @@ const WaypointCreateSchema = z.object({
description: z.string().optional(),
lat: z.number({coerce: true}).min(-90).max(90),
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),
photos: z.array(z.string()).default([]),
}) satisfies ZodType<Partial<Waypoint>>
@@ -17,7 +19,8 @@ const WaypointUpdateSchema = z.object({
description: z.string().optional(),
lat: z.number({coerce: true}).min(-90).max(90).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.string().optional(),
"photos+": z.string().optional(),

View File

@@ -1,3 +1,4 @@
import type { icons } from "$lib/util/icon_util";
import * as M from "maplibre-gl";
class Waypoint {
@@ -6,14 +7,15 @@ class Waypoint {
description?: string;
lat: number;
lon: number;
icon?: string;
distance_from_start?: number;
icon?: typeof icons[number];
marker?: M.Marker;
photos: string[];
_photos?: File[];
author: string;
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.name = params?.name ?? "";

View File

@@ -38,10 +38,11 @@ export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker
})
const content = document.createElement("div");
content.className = "p-2"
const spanElement = document.createElement("span");
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}`)
spanElement.appendChild(iconElement);
@@ -91,6 +92,7 @@ export function createAnchorMarker(lat: number, lon: number, index: number,
const popup = new M.Popup()
const popupContent = document.createElement("div");
popupContent.className = "py-3 pl-3"
const anchorH = document.createElement("h5")
anchorH.classList.add("text-base", "font-medium");
anchorH.textContent = get(_)("route-point") + " #" + index;
@@ -140,7 +142,7 @@ export function createPopupFromTrail(trail: Trail) {
: get(theme) === "light"
? emptyStateTrailLight
: emptyStateTrailDark;
const popup = new M.Popup({ maxWidth: "320px" });
const popup = new M.Popup({ maxWidth: "420px" });
// Create a container element for the popup content
const linkElement = document.createElement("a");
linkElement.href = `/map/trail/${trail.id}`; // Set href safely
@@ -148,7 +150,7 @@ export function createPopupFromTrail(trail: Trail) {
// Create a list item element
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
const imageContainer = document.createElement("div");
@@ -156,13 +158,14 @@ export function createPopupFromTrail(trail: Trail) {
// Create the image element
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.alt = ""; // Always include a safe alt attribute
imageContainer.appendChild(img);
// Create the text container
const textContainer = document.createElement("div");
textContainer.className = "py-2"
// Add trail name
const trailName = document.createElement("h4");
@@ -193,7 +196,7 @@ export function createPopupFromTrail(trail: Trail) {
// Create the grid container for additional stats
const statsContainer = document.createElement("div");
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 = [
{ icon: "fa-left-right", value: formatDistance(trail.distance) },

View File

@@ -943,6 +943,7 @@ export class ElevationProfile {
if (distance < minDistances[waypointIndex]) {
minDistances[waypointIndex] = distance;
this.waypointPositions[waypointIndex] = this.cumulatedDistanceAdjustedUnit[i];
waypoint.distance_from_start = this.cumulatedDistanceAdjustedUnit[i] * 1000;
}
});

View File

@@ -16,6 +16,7 @@ export async function GET(event: RequestEvent) {
t.expand = {} as any
}
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)
} catch (e: any) {

View File

@@ -27,6 +27,9 @@ export async function GET(event: RequestEvent) {
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)
} catch (e: any) {
throw handleError(e)

View File

@@ -73,7 +73,7 @@
const trailItems = r[0].hits.map((t: TrailSearchResult) => ({
text: t.name,
description: `Trail ${t.location.length ? ", " + t.location : ""}`,
value: t,
value: t.id,
icon: "route",
}));
const listItems = r[1].hits.map((t: ListSearchResult) => ({
@@ -93,7 +93,9 @@
}
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}`);
} else {
map?.setCenter([item.value.lon, item.value.lat]);
@@ -220,7 +222,7 @@
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
<div
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="flex items-center gap-2 md:gap-4">

View File

@@ -13,10 +13,10 @@
<title>{$trail.name} | {$_("map")} | wanderer</title>
</svelte:head>
<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>
</div>
<div id="trail-details" class="sticky top-[62px]">
<div id="trail-details">
<MapWithElevationMaplibre
trails={[$trail]}
waypoints={$trail.expand?.waypoints}
@@ -28,11 +28,11 @@
</main>
<style>
#trail-details {
#trail-details, #panel {
height: calc(100vh);
}
@media only screen and (min-width: 768px) {
#trail-details {
#trail-details, #panel {
height: calc(100vh - 124px);
}
}