Files
wanderer/web/src/lib/components/trail/trail_card.svelte
Brian Nelson 9b6522ec21 Enhance Tags on TrailCard component with toggle support (#285)
* Updated trail card to show first 2 tags and made tags smaller

* fixes ux and adds i18n

---------

Co-authored-by: Christian Beutel <>
2025-06-26 13:22:54 +02:00

223 lines
7.4 KiB
Svelte

<script lang="ts">
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import type { Trail } from "$lib/models/trail";
import { theme } from "$lib/stores/theme_store";
import { currentUser } from "$lib/stores/user_store";
import { getFileURL, isVideoURL } from "$lib/util/file_util";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import type { MouseEventHandler } from "svelte/elements";
import Chip from "../base/chip.svelte";
import { fade, slide } from "svelte/transition";
interface Props {
trail: Trail;
fullWidth?: boolean;
onmouseenter?: MouseEventHandler<HTMLDivElement>;
onmouseleave?: MouseEventHandler<HTMLDivElement>;
}
let {
trail,
fullWidth = false,
onmouseenter,
onmouseleave,
}: Props = $props();
let thumbnail = $derived(
trail.photos.length
? getFileURL(
trail,
trail.photos.at(trail.thumbnail ?? 0) ?? trail.photos[0],
"600x0",
)
: $theme === "light"
? emptyStateTrailLight
: emptyStateTrailDark,
);
let trailIsShared = $derived(
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0,
);
// expand and collapse the tags
let expandedTags = $state(false);
function toggleExpandTags(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
expandedTags = !expandedTags;
}
</script>
<div
class="trail-card relative rounded-2xl border border-input-border min-w-72 h-[386px] {fullWidth
? ''
: 'lg:w-72'} cursor-pointer flex flex-col"
{onmouseenter}
{onmouseleave}
role="listitem"
>
<div
class="relative w-full basis-full max-h-48 overflow-hidden rounded-t-2xl"
>
{#if isVideoURL(thumbnail)}
<!-- svelte-ignore a11y_media_has_caption -->
<video
id="header-img"
class="w-full h-full object-cover"
autoplay
loop
src={thumbnail}
></video>
{:else}
<img
loading="lazy"
class="w-full h-full"
id="header-img"
src={thumbnail}
alt=""
/>
{/if}
</div>
{#if $currentUser && trail.like_count > 0}
<div
class="flex absolute items-center justify-center top-4 left-4 bg-background w-8 h-8 rounded-full"
>
<span class="tooltip" data-title={$_("likes")}>
<i class="fa fa-heart"></i>
</span>
<div
class="absolute pointer-events-none left-5 -top-1 text-xs rounded-full bg-menu-background px-1 text-center"
>
{trail.like_count}
</div>
</div>
{/if}
{#if (trail.public || trailIsShared) && $currentUser}
<div
class="flex absolute top-4 right-4 {trail.public && trailIsShared
? 'w-14'
: 'w-8'} h-8 rounded-full items-center justify-center bg-background text-content"
>
{#if trail.public && $currentUser}
<span
class="tooltip"
class:mr-2={trail.public && trailIsShared}
data-title={$_("public")}
>
<i class="fa fa-globe"></i>
</span>
{/if}
{#if trail.expand?.trail_share_via_trail?.length}
<span class="tooltip" data-title={$_("shared")}>
<i class="fa fa-share-nodes"></i>
</span>
{/if}
</div>
{/if}
<div class="p-4">
<div>
<h4 class="font-semibold text-lg line-clamp-2">{trail.name}</h4>
{#if trail.date}
<p class="text-xs text-gray-500 mb-3">
{new Date(trail.date).toLocaleDateString(undefined, {
month: "long",
day: "2-digit",
year: "numeric",
timeZone: "UTC",
})}
</p>
{/if}
{#if trail.expand?.author}
<p class="text-xs text-gray-500 mb-3">
{$_("by")}
<img
class="rounded-full w-5 aspect-square mx-1 inline"
src={trail.expand.author.icon ||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
alt="avatar"
/>
{trail.expand.author.preferred_username}{trail.expand.author
.isLocal
? ""
: "@" + trail.expand.author.domain}
</p>
{/if}
{#if trail.tags.length}
<div class="flex flex-wrap gap-1 mb-3 items-center">
{#each expandedTags ? trail.tags : trail.tags.slice(0, 2) as t}
<Chip text={t} closable={false} primary={false}></Chip>
{/each}
{#if trail.tags.length > 2}
<button
onclick={toggleExpandTags}
class="text-sm text-gray-500 hover:underline focus:outline-none"
type="button"
>
{#if expandedTags}
{$_('show-less')}
{:else}
+{trail.tags.length - 2} {$_('more')}
{/if}
</button>
{/if}
</div>
{/if}
<div class="flex gap-x-4">
{#if trail.location}
<h5>
<i class="fa fa-location-dot mr-3"></i>{trail.location}
</h5>
{/if}
<h5>
<i class="fa fa-gauge mr-3"></i>{$_(
trail.difficulty ?? "?",
)}
</h5>
</div>
</div>
<div
class="grid grid-cols-2 mt-2 gap-1 text-sm text-gray-500 whitespace-nowrap"
>
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance,
)}</span
>
<span
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
trail.duration,
)}</span
>
<span
><i class="fa fa-arrow-trend-up mr-2"></i>{formatElevation(
trail.elevation_gain,
)}</span
>
<span
><i class="fa fa-arrow-trend-down mr-2"></i>{formatElevation(
trail.elevation_loss,
)}</span
>
</div>
</div>
</div>
<style>
.trail-card #header-img {
object-fit: cover;
transition: 0.25s ease;
}
.trail-card:hover #header-img {
scale: 1.075;
}
</style>