Feature/trail table view (#138)

* add "Table"

* add trails table view

* style table header

* add author image, adjust table layout

* tooltip for author image

* i18n for author tooltip

* trail sorting by duration and author

* remove sort by elevation loss, add sort by duration instead

* improve table layout

* fix meilisearch migration

* revert removing elevation_loss from sortOptions

* add badge for public trails

* hide public badge on small screens

* adds share info button to trail_table
This commit is contained in:
tofublock
2024-12-13 20:39:42 +01:00
committed by GitHub
parent d1a416629b
commit 85729e9728
12 changed files with 299 additions and 39 deletions

View File

@@ -7,6 +7,7 @@
import EmptyStateSearch from "../empty_states/empty_state_search.svelte";
import TrailCard from "./trail_card.svelte";
import TrailListItem from "./trail_list_item.svelte";
import TrailTable from "./trail_table.svelte";
import SkeletonCard from "../base/skeleton_card.svelte";
import SkeletonListItem from "../base/skeleton_list_item.svelte";
@@ -22,6 +23,7 @@
const displayOptions: SelectItem[] = [
{ text: $_("card", { values: { n: 2 } }), value: "cards" },
{ text: $_("list", { values: { n: 1 } }), value: "list" },
{ text: $_("table"), value: "table" },
];
let selectedDisplayOption = displayOptions[0].value;
@@ -29,13 +31,14 @@
let dispatch = createEventDispatcher();
const sortOptions: SelectItem[] = [
{ text: $_("alphabetical"), value: "name" },
{ text: $_("creation-date"), value: "created" },
{ text: $_("date"), value: "date" },
{ text: $_("difficulty"), value: "difficulty" },
{ text: $_("name"), value: "name" },
{ text: $_("distance"), value: "distance" },
{ text: $_("duration"), value: "duration" },
{ text: $_("difficulty"), value: "difficulty" },
{ text: $_("elevation-gain"), value: "elevation_gain" },
{ text: $_("elevation-loss"), value: "elevation_loss" },
{ text: $_("creation-date"), value: "created" },
{ text: $_("date"), value: "date" },
];
onMount(() => {
@@ -81,6 +84,19 @@
localStorage.setItem("sort_order", filter.sortOrder);
dispatch("update", filter);
}
function handleSortUpdate(sort: any) {
if (!filter) {
return;
}
if (sort.detail === filter.sort) {
setSortOrder();
} else {
filter.sort = sort.detail;
filter.sortOrder = "+";
setSort();
}
}
</script>
<div class="min-w-0">
@@ -94,21 +110,23 @@
</div>
{#if filter}
<div class="shrink-0">
<p class="text-sm text-gray-500 pb-2">{$_("sort")}</p>
<div class="flex items-center gap-2">
<Select
bind:value={filter.sort}
items={sortOptions}
on:change={setSort}
></Select>
<button
id="sort-order-btn"
class="btn-icon"
class:rotated={filter.sortOrder == "-"}
on:click={() => setSortOrder()}
><i class="fa fa-arrow-up"></i></button
>
</div>
{#if selectedDisplayOption !== "table"}
<p class="text-sm text-gray-500 pb-2">{$_("sort")}</p>
<div class="flex items-center gap-2">
<Select
bind:value={filter.sort}
items={sortOptions}
on:change={setSort}
></Select>
<button
id="sort-order-btn"
class="btn-icon"
class:rotated={filter.sortOrder == "-"}
on:click={() => setSortOrder()}
><i class="fa fa-arrow-up"></i></button
>
</div>
{/if}
</div>
{/if}
<div class="shrink-0">
@@ -124,35 +142,51 @@
<div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full">
{#if loading}
{#each { length: 12 } as _, index}
{#if selectedDisplayOption === "cards"}
{#if selectedDisplayOption === "table"}
<TrailTable trails={null} tableHeader={sortOptions}
></TrailTable>
{:else}
{#each { length: 12 } as _, index}
{#if selectedDisplayOption === "cards"}
<div class="flex-1">
<SkeletonCard></SkeletonCard>
</div>
{:else}
<SkeletonListItem></SkeletonListItem>
{/if}
{/each}
{:else if selectedDisplayOption === "list"}
<SkeletonListItem></SkeletonListItem>
{/if}
{/each}
{/if}
{:else}
{#if trails.length == 0}
<div class="flex flex-col basis-full items-center">
<EmptyStateSearch width={356}></EmptyStateSearch>
</div>
{/if}
{#each trails as trail}
<a
class="max-w-full flex-1"
class:basis-full={selectedDisplayOption === "list"}
href="/trail/view/{trail.id}"
data-sveltekit-preload-data="off"
>
{#if selectedDisplayOption === "cards"}
<TrailCard {trail}></TrailCard>
{:else}
<TrailListItem {trail}></TrailListItem>
{/if}
</a>
{/each}
{#if selectedDisplayOption === "table"}
<TrailTable
{trails}
tableHeader={sortOptions.filter(
(option) => option.value !== "elevation_loss",
)}
{filter}
on:sort={(sort) => handleSortUpdate(sort)}
></TrailTable>
{:else}
{#each trails as trail}
<a
class="max-w-full flex-1"
class:basis-full={selectedDisplayOption === "list"}
href="/trail/view/{trail.id}"
data-sveltekit-preload-data="off"
>
{#if selectedDisplayOption === "cards"}
<TrailCard {trail}></TrailCard>
{:else}
<TrailListItem {trail}></TrailListItem>
{/if}
</a>
{/each}
{/if}
{/if}
</div>
<Pagination

View File

@@ -0,0 +1,181 @@
<script lang="ts">
import type { Trail, TrailFilter } from "$lib/models/trail";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import type { SelectItem } from "../base/select.svelte";
import { goto } from "$app/navigation";
import { createEventDispatcher } from "svelte";
import { getFileURL } from "$lib/util/file_util";
import ShareInfo from "../share_info.svelte";
export let tableHeader: SelectItem[];
export let trails: Trail[] | null = null;
export let filter: TrailFilter | null = null;
let dispatch = createEventDispatcher();
function getColumnWidth(columnValue: string): string {
switch (columnValue) {
case "name":
return "w-[25%]";
case "distance":
return "w-[15%]";
case "duration":
case "difficulty":
return "w-[11%]";
case "elevation_gain":
case "created":
case "date":
return "w-[5%]";
default:
return "";
}
}
</script>
<div
class="table-container w-full border border-input-border rounded-xl overflow-x-scroll"
>
<table class="w-full">
<thead>
<tr class="bg-secondary-hover">
{#each tableHeader as column}
<th
class="p-4 text-left text-sm font-medium {getColumnWidth(
column.value,
)}"
on:click={() => dispatch("sort", column.value)}
>
<div class="cursor-pointer">
{column.text}
{#if filter && filter.sort === column.value}
<i
id="sort-order-btn"
class="fa fa-arrow-up"
class:rotated={filter.sortOrder == "-"}
></i>
{/if}
</div>
</th>
{/each}
</tr>
</thead>
<tbody>
{#if trails}
{#each trails as trail}
<tr
class="border-t border-input-border cursor-pointer hover:bg-secondary-hover transition-colors"
on:click={() => goto(`/trail/view/${trail.id}`)}
>
<td
class="flex justify-between items-center text-sm relative"
>
<div class="p-4 w-[75%]" title={trail.name}>
{trail.name}
</div>
<div class="flex flex-col items-center">
{#if trail.expand && trail.expand.author}
<div class="author-icon">
<img
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} ${trail.expand.author.username}`}
class="rounded-full w-5 aspect-square mx-1 inline"
src={getFileURL(
trail.expand.author,
trail.expand.author.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
</div>
{/if}
<div class="flex gap-x-1">
{#if trail.public}
<div
class="public-icon tooltip"
data-title={$_("public")}
>
<i class="fa fa-globe"></i>
</div>
{/if}
{#if trail.expand?.trail_share_via_trail?.length}
<ShareInfo type="trail" subject={trail}
></ShareInfo>
{/if}
</div>
</div>
</td>
<td class="p-4 text-sm">
{formatDistance(trail.distance)}
</td>
<td class="p-4 text-sm">
{formatTimeHHMM(trail.duration)}
</td>
<td class="p-4 text-sm">
{trail.difficulty}
</td>
<td class="p-4 text-sm">
{formatElevation(trail.elevation_gain)}
</td>
<td class="p-4 text-sm">
{#if trail.created}
{new Date(trail.created).toLocaleDateString(
undefined,
{
month: "2-digit",
day: "2-digit",
year: "numeric",
timeZone: "UTC",
},
)}
{/if}
</td>
<td class="p-4 text-sm">
{#if trail.date}
{new Date(trail.date).toLocaleDateString(
undefined,
{
month: "2-digit",
day: "2-digit",
year: "numeric",
timeZone: "UTC",
},
)}
{/if}
</td>
</tr>
{/each}
{:else}
{#each { length: 12 } as _, index}
<tr
class="border-t border-input-border cursor-pointer bg-secondary-hover transition-colors animate-pulse"
>
{#each { length: tableHeader.length } as _, index}
<td class="p-4 text-sm"
><div
class="h-4 bg-menu-item-background-focus rounded w-3/4"
></div></td
>
{/each}
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
<style>
.table-container {
container-type: inline-size;
}
@container (max-width: 660px) {
th:nth-last-child(-n + 2),
td:nth-last-child(-n + 2) {
display: none;
}
}
</style>

View File

@@ -213,6 +213,7 @@
"speed": "Geschwindigkeit",
"stop-drawing": "Zeichnen beenden",
"summit-book": "Gipfelbuch",
"table": "Tabelle",
"text": "Text",
"tilesets": "Tilesets",
"trail": "{n, plural, =1 {Route} other {Routen}}",

View File

@@ -213,6 +213,7 @@
"speed": "Speed",
"stop-drawing": "Stop drawing",
"summit-book": "Summit Book",
"table": "Table",
"text": "Text",
"tilesets": "Custom tilesets",
"trail": "{n, plural, =1 {Trail} other {Trails}}",

View File

@@ -213,6 +213,7 @@
"speed": "",
"stop-drawing": "",
"summit-book": "Livre du sommet",
"table": "Tableau",
"text": "Texte",
"tilesets": "",
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",

View File

@@ -213,6 +213,7 @@
"speed": "",
"stop-drawing": "",
"summit-book": "Csúcspont könyv",
"table": "Táblázat",
"text": "Szöveg",
"tilesets": "",
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",

View File

@@ -213,6 +213,7 @@
"speed": "Velocità",
"stop-drawing": "Smettere di disegnare",
"summit-book": "Libro di vetta",
"table": "Tavolo",
"text": "Testo",
"tilesets": "",
"trail": "{n, plural, =1 {Percorso} other {Percorsi}}",

View File

@@ -213,6 +213,7 @@
"speed": "",
"stop-drawing": "",
"summit-book": "Bergtopboek",
"table": "Tabel",
"text": "Tekst",
"tilesets": "",
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",

View File

@@ -213,6 +213,7 @@
"speed": "",
"stop-drawing": "",
"summit-book": "Logbook",
"table": "Tabela",
"text": "Tekst",
"tilesets": "",
"trail": "{n, plural, =1 {Szlak} other {Szlaki}}",

View File

@@ -213,6 +213,7 @@
"speed": "",
"stop-drawing": "",
"summit-book": "Livro da cimeira",
"table": "Tabela",
"text": "Texto",
"tilesets": "",
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",

View File

@@ -213,6 +213,7 @@
"speed": "",
"stop-drawing": "",
"summit-book": "详细日程",
"table": "表格",
"text": "文本",
"tilesets": "",
"trail": "{n, plural, =1 {路线} other {路线}}",