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

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