adds loaders for trail cards and lists

This commit is contained in:
Christian Beutel
2024-12-02 20:21:25 +01:00
parent a5a7532559
commit 4d6657b64e
10 changed files with 150 additions and 76 deletions

View File

@@ -0,0 +1,14 @@
<div
class="skeleton-card relative rounded-2xl sm:w-72 animate-pulse flex flex-col w-80 p-4 bg-menu-background border border-input-border"
aria-hidden="true"
>
<!-- Image placeholder -->
<div class="h-48 bg-menu-item-background-focus rounded-md"></div>
<!-- Text placeholders -->
<div class="mt-4 space-y-3">
<div class="h-4 bg-menu-item-background-focus rounded w-3/4"></div>
<div class="h-4 bg-menu-item-background-focus rounded w-full"></div>
<div class="h-4 bg-menu-item-background-focus rounded w-1/2"></div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div
class="skeleton-list-item animate-pulse flex items-center gap-4 p-4 bg-menu-background rounded-xl border border-input-border basis-full"
aria-hidden="true"
>
<!-- Avatar placeholder -->
<div class="h-28 w-28 bg-menu-item-background-focus rounded-xl shrink-0"></div>
<!-- Text placeholders -->
<div class="basis-full shrink-0 space-y-2">
<div class="h-4 bg-menu-item-background-focus rounded w-1/3"></div>
<div class="h-4 bg-menu-item-background-focus rounded w-1/5"></div>
<div class="h-4 bg-menu-item-background-focus rounded w-2/3"></div>
<div class="h-4 bg-menu-item-background-focus rounded w-1/2"></div>
</div>
</div>

View File

@@ -355,6 +355,7 @@
);
epc?.showProfile();
}
showWaypoints();
flyToBounds();
}
@@ -367,6 +368,7 @@
if (showElevation) {
epc?.hideProfile();
}
hideWaypoints();
}
function addStartEndMarkers(
@@ -418,6 +420,26 @@
layers[id]?.startMarker?.togglePopup();
}
function showWaypoints() {
if (!map) {
return;
}
for (const waypoint of trails[activeTrail]?.expand.waypoints ?? []) {
const marker = createMarkerFromWaypoint(waypoint);
marker.addTo(map);
markers.push(marker);
}
}
function hideWaypoints() {
if (!map) {
return;
}
for (const m of markers) {
m.remove()
}
}
onMount(async () => {
const initialState = {
lng: 0,
@@ -465,7 +487,7 @@
(s) => s.text === localStorage.getItem("layer"),
);
if(preferredMapStyleIndex == -1) {
if (preferredMapStyleIndex == -1) {
preferredMapStyleIndex = 0;
}
@@ -567,7 +589,7 @@
addTrailLayer(t, t.id ?? i.toString(), data?.at(i));
});
if (showTerrain && $page.data.settings?.terrain) {
if (showTerrain && $page.data.settings?.terrain && !map?.getSource("terrain")) {
map!.addSource("terrain", {
type: "raster-dem",
url: $page.data.settings.terrain,
@@ -600,11 +622,7 @@
dispatch("init", map);
});
for (const waypoint of trails[activeTrail]?.expand.waypoints ?? []) {
const marker = createMarkerFromWaypoint(waypoint);
marker.addTo(map);
markers.push(marker);
}
showWaypoints();
});
onDestroy(() => {

View File

@@ -7,6 +7,8 @@
import EmptyStateSearch from "../empty_states/empty_state_search.svelte";
import TrailCard from "./trail_card.svelte";
import TrailListItem from "./trail_list_item.svelte";
import SkeletonCard from "../base/skeleton_card.svelte";
import SkeletonListItem from "../base/skeleton_list_item.svelte";
export let filter: TrailFilter | null = null;
export let trails: Trail[];
@@ -15,6 +17,8 @@
totalPages: 1,
};
export let loading: boolean = false;
const displayOptions: SelectItem[] = [
{ text: $_("card", { values: { n: 2 } }), value: "cards" },
{ text: $_("list", { values: { n: 1 } }), value: "list" },
@@ -58,7 +62,7 @@
}
function setSort() {
if(!filter) {
if (!filter) {
return;
}
localStorage.setItem("sort", filter.sort);
@@ -66,7 +70,7 @@
}
function setSortOrder() {
if(!filter) {
if (!filter) {
return;
}
if (filter.sortOrder === "+") {
@@ -119,25 +123,35 @@
</div>
<div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full">
{#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"
class:basis-full={selectedDisplayOption === "list"}
href="/trail/view/{trail.id}"
data-sveltekit-preload-data="off"
>
{#if loading}
{#each { length: 12 } as _, index}
{#if selectedDisplayOption === "cards"}
<TrailCard {trail}></TrailCard>
<SkeletonCard></SkeletonCard>
{:else}
<TrailListItem {trail}></TrailListItem>
<SkeletonListItem></SkeletonListItem>
{/if}
</a>
{/each}
{/each}
{: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"
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}
</div>
<Pagination
page={pagination.page}

View File

@@ -26,7 +26,7 @@ export async function trails_index(perPage: number = 21, random: boolean = false
const response = await r.json()
if (r.ok) {
if(setStore) {
if (setStore) {
trails.set(response.items);
}
return response.items;
@@ -40,7 +40,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
let r = await f("/api/v1/search/trails", {
method: "POST",
body: JSON.stringify({ q: filter.q, options: { filter: filterText, sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`], hitsPerPage: 21, page: page } }),
body: JSON.stringify({ q: filter.q, options: { filter: filterText, sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`], hitsPerPage: 12, page: page } }),
});
const result = await r.json();
@@ -52,8 +52,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
const trailIds = result.hits.map((h: Record<string, any>) => h.id);
if (trailIds.length == 0) {
trails.set([]);
return [];
return { trails: [], ...result };
}
r = await f('/api/v1/trail?' + new URLSearchParams({
@@ -66,8 +65,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
const response = await r.json()
if (r.ok) {
trails.set(response.items);
return result;
return { trails: response.items, ...result };
} else {
throw new ClientResponseError(response)
}
@@ -85,7 +83,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
method: "POST",
body: JSON.stringify({
q: "", options: {
limit: 400,
limit: 100,
filter: [
`_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`,
filterText
@@ -99,8 +97,9 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
if (trailIds.length == 0) {
const currentTrails: Trail[] = get(trails);
trails.set([]);
return compareObjectArrays<Trail>(currentTrails, []);
const comparison = compareObjectArrays<Trail>(currentTrails, []);
return { trails: [], ...comparison }
}
r = await fetch('/api/v1/trail?' + new URLSearchParams({
@@ -127,11 +126,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
const comparison = compareObjectArrays<Trail>(get(trails), response.items)
if (comparison.added.length || comparison.deleted.length || comparison.updated.length) {
trails.set(response.items);
}
return comparison;
return { trails: response.items, ...comparison };
} else {
throw new ClientResponseError(response)
}

View File

@@ -179,6 +179,7 @@
}}
bind:activeTrail={activeTrailIndex}
showInfoPopup={true}
showTerrain={true}
></MapWithElevationMaplibre>
</div>
<div class="min-w-0" class:hidden={showMap}>

View File

@@ -5,6 +5,7 @@
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
import SkeletonCard from "$lib/components/base/skeleton_card.svelte";
import EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte";
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import TrailCard from "$lib/components/trail/trail_card.svelte";
@@ -16,16 +17,15 @@
TrailFilter,
} from "$lib/models/trail";
import { categories } from "$lib/stores/category_store";
import {
trails,
trails_search_bounding_box,
} from "$lib/stores/trail_store";
import { trails_search_bounding_box } from "$lib/stores/trail_store";
import { country_codes } from "$lib/util/country_code_util";
import * as M from "maplibre-gl";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
import { slide } from "svelte/transition";
let trails: Trail[] = [];
let map: M.Map;
let mapWithElevation: MapWithElevationMaplibre;
let searchDropdownItems: SearchItem[] = [];
@@ -39,6 +39,8 @@
const MIN_ZOOM = 6;
let loading: boolean = false;
onMount(async () => {});
async function search(q: string) {
@@ -92,12 +94,15 @@
}
async function searchTrails(northEast: M.LngLat, southWest: M.LngLat) {
loading = true;
const changes = await trails_search_bounding_box(
northEast,
southWest,
filter,
map.getZoom() > MIN_ZOOM,
);
trails = changes.trails;
loading = false;
}
function handleTrailCardMouseEnter(trail: Trail) {
@@ -147,9 +152,10 @@
$page.url.searchParams.has("lat") &&
$page.url.searchParams.has("lon")
) {
console.log($page.url.searchParams.get("lon"), $page.url.searchParams.get("lat"));
map.setCenter([parseFloat($page.url.searchParams.get("lon")!), parseFloat($page.url.searchParams.get("lat")!)]);
map.setCenter([
parseFloat($page.url.searchParams.get("lon")!),
parseFloat($page.url.searchParams.get("lat")!),
]);
map.setZoom(14);
} else if (settings && settings.mapFocus == "trails") {
const boundingBox: M.LngLatBoundsLike = [
@@ -165,8 +171,6 @@
map.setCenter([settings.location.lon, settings.location.lat]);
map.setZoom(12);
} else {
console.log("here");
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
@@ -228,21 +232,29 @@
</div>
{#if !showFilter && (!showMap || (browser && window.innerWidth >= 768))}
{#if $trails.length == 0}
<EmptyStateSearch></EmptyStateSearch>
{#if loading}
{#each { length: 4 } as _, index}
<SkeletonCard></SkeletonCard>
{/each}
{:else}
{#if trails.length == 0}
<EmptyStateSearch></EmptyStateSearch>
{/if}
{#each trails as trail, i}
<a
data-sveltekit-preload-data="off"
href="map/trail/{trail.id}"
>
<TrailCard
{trail}
on:mouseenter={() =>
handleTrailCardMouseEnter(trail)}
on:mouseleave={() =>
handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
{/each}
{/if}
{#each $trails as trail, i}
<a
data-sveltekit-preload-data="off"
href="map/trail/{trail.id}"
>
<TrailCard
{trail}
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
on:mouseleave={() => handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
{/each}
{/if}
</div>
<div
@@ -252,7 +264,7 @@
<MapWithElevationMaplibre
on:moveend={handleMapMove}
on:init={handleMapInit}
trails={$trails}
{trails}
showElevation={false}
showInfoPopup={true}
activeTrail={-1}

View File

@@ -32,7 +32,5 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
await categories_index(fetch)
trails.set([])
return { filter: filter, boundingBox: boundingBox }
};

View File

@@ -3,17 +3,21 @@
import { page } from "$app/stores";
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte";
import type { TrailFilter } from "$lib/models/trail";
import { categories } from "$lib/stores/category_store";
import { trails, trails_search_filter } from "$lib/stores/trail_store";
import type { Trail, TrailFilter } from "$lib/models/trail";
import { trails_search_filter } from "$lib/stores/trail_store";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let filterExpanded: boolean = true;
let loading: boolean = false;
const filter: TrailFilter = $page.data.filter;
const pagination: { page: number; totalPages: number } =
$page.data.pagination;
const pagination: { page: number; totalPages: number } = {
page: 1,
totalPages: 1,
};
let trails: Trail[] = [];
onMount(() => {
if (window.innerWidth < 768) {
@@ -22,14 +26,18 @@
});
async function handleFilterUpdate() {
loading = true;
const response = await trails_search_filter(filter, pagination.page);
trails = response.trails;
pagination.page = response.page;
pagination.totalPages = response.totalPages;
loading = false;
}
async function paginate(page: number) {
pagination.page = page;
const response = await trails_search_filter(filter, page);
trails = response.trails;
$page.url.searchParams.set("page", page.toString());
goto(`?${$page.url.searchParams.toString()}`);
}
@@ -50,7 +58,8 @@
></TrailFilterPanel>
<TrailList
{filter}
trails={$trails}
{loading}
{trails}
{pagination}
on:update={() => handleFilterUpdate()}
on:pagination={(e) => paginate(e.detail)}

View File

@@ -1,4 +1,4 @@
import type { TrailFilter } from "$lib/models/trail";
import type { Trail, TrailFilter } from "$lib/models/trail";
import { categories_index } from "$lib/stores/category_store";
import { trails_get_filter_values, trails_search_filter } from "$lib/stores/trail_store";
import type { ServerLoad } from "@sveltejs/kit";
@@ -32,13 +32,10 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
if (paramCategory) {
filter.category.push(paramCategory);
}
const page = url.searchParams.get("page") ?? "1";
const response = await trails_search_filter(filter, parseInt(page), fetch);
const categories = await categories_index(fetch)
return {
categories,
filter: filter, pagination: { page: response.page, totalPages: response.totalPages }
filter: filter
};
};