updates trail panel view
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user