From 5e4ccc59c0692aa58e34ed3f4a20a6775319e54e Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Mon, 5 Feb 2024 22:37:11 +0100 Subject: [PATCH] finishes trails page --- .../lib/components/base/double_slider.svelte | 8 +- web/src/lib/components/base/dropdown.svelte | 18 +- web/src/lib/components/base/search.svelte | 2 +- web/src/lib/components/base/select.svelte | 19 +- web/src/lib/components/base/slider.svelte | 8 +- .../lib/components/trail/trail_card.svelte | 16 +- .../components/trail/trail_list_item.svelte | 74 +++++++ web/src/lib/models/trail.ts | 7 +- web/src/lib/stores/trail_store.ts | 44 ++++- web/src/lib/util/format_util.ts | 2 +- web/src/routes/+page.svelte | 18 +- web/src/routes/trails/+page.svelte | 180 +++++++++++++++--- 12 files changed, 343 insertions(+), 53 deletions(-) create mode 100644 web/src/lib/components/trail/trail_list_item.svelte diff --git a/web/src/lib/components/base/double_slider.svelte b/web/src/lib/components/base/double_slider.svelte index baad92dd..8543b1d2 100644 --- a/web/src/lib/components/base/double_slider.svelte +++ b/web/src/lib/components/base/double_slider.svelte @@ -1,7 +1,7 @@ diff --git a/web/src/lib/components/base/dropdown.svelte b/web/src/lib/components/base/dropdown.svelte index 2a8cf09c..5b61bd37 100644 --- a/web/src/lib/components/base/dropdown.svelte +++ b/web/src/lib/components/base/dropdown.svelte @@ -1,7 +1,15 @@ + + +
@@ -15,6 +31,7 @@ {name} class="bg-gray-50 h-10 w-full px-4 border-r-8 border-transparent outline outline-1 outline-gray-200 rounded-md focus:outline-primary transition-colors" bind:value + on:change={(e) => onChange(e.target)} > {#each items as item} diff --git a/web/src/lib/components/base/slider.svelte b/web/src/lib/components/base/slider.svelte index 8d2f7d82..5f982230 100644 --- a/web/src/lib/components/base/slider.svelte +++ b/web/src/lib/components/base/slider.svelte @@ -1,7 +1,7 @@ diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index 299105e4..a6a084a2 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -1,5 +1,7 @@
@@ -22,7 +36,7 @@

{trail.name}

{#if $currentUser && $currentUser.id == trail.author && mode == "edit"} - + handleDropdownClick(trail, e.detail)} items={dropdownItems}> {/if}
{trail.location}
diff --git a/web/src/lib/components/trail/trail_list_item.svelte b/web/src/lib/components/trail/trail_list_item.svelte new file mode 100644 index 00000000..9ddc2fc3 --- /dev/null +++ b/web/src/lib/components/trail/trail_list_item.svelte @@ -0,0 +1,74 @@ + + +
  • +
    + +
    +
    +
    +

    {trail.name}

    + + {#if $currentUser && $currentUser.id == trail.author} + handleDropdownClick(trail, e.detail)} + items={dropdownItems} + > + {/if} +
    +
    {trail.location}
    +
    + {formatMeters( + trail.distance, + )} + {formatMeters( + trail.elevation_gain, + )} + {formatTimeHHMM( + trail.duration, + )} +
    +

    + {trail.description} +

    +
    +
  • diff --git a/web/src/lib/models/trail.ts b/web/src/lib/models/trail.ts index 810ff7e4..821055c8 100644 --- a/web/src/lib/models/trail.ts +++ b/web/src/lib/models/trail.ts @@ -16,6 +16,7 @@ class Trail { thumbnail?: string; photos: string[]; gpx?: string; + created?: string; expand: { category?: Category; waypoints: Waypoint[] @@ -46,6 +47,7 @@ class Trail { summit_logs?: SummitLog[], tags?: string[], description?: string + created?: string } ) { @@ -68,6 +70,7 @@ class Trail { } this.tags = params?.tags ?? [] this.description = params?.description ?? ""; + this.created = params?.created; this._photoFiles = []; } } @@ -91,13 +94,15 @@ interface TrailFilter { near: { lat?: number, lon?: number, - distance: number + radius: number } distanceMin: number, distanceMax: number, eleavationGainMin: number; elevationGainMax: number; completed?: boolean; + sort: "name" | "distance" | "elevation_gain" | "created"; + sortOrder: "+" | "-" } export { Trail, trailSchema }; diff --git a/web/src/lib/stores/trail_store.ts b/web/src/lib/stores/trail_store.ts index 1df608c8..91e619c9 100644 --- a/web/src/lib/stores/trail_store.ts +++ b/web/src/lib/stores/trail_store.ts @@ -1,5 +1,5 @@ import type { SummitLog } from "$lib/models/summit_log"; -import { Trail } from "$lib/models/trail"; +import { Trail, type TrailFilter } from "$lib/models/trail"; import type { Waypoint } from "$lib/models/waypoint"; import { pb } from "$lib/pocketbase"; import { getFileURL } from "$lib/util/file_util"; @@ -15,7 +15,7 @@ export const trail: Writable = writable(new Trail("")); export const editTrail: Writable = writable(new Trail("")); export async function trails_index() { - const response: Trail[] = (await pb.collection('trails').getList(1, 5, { expand: "category,waypoints,summit_logs" })).items + const response: Trail[] = (await pb.collection('trails').getList(1, 5, {expand: "category,waypoints,summit_logs" })).items for (const trail of response) { setFileURLs(trail); @@ -26,6 +26,42 @@ export async function trails_index() { return response; } +export async function trails_search(filter: TrailFilter) { + let filterText: string = `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.eleavationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`; + + if (filter.category.length > 0) { + filterText += ` AND category IN [${filter.category.join(",")}]`; + } + if (filter.completed !== undefined) { + filterText += ` AND completed = ${filter.completed}`; + } + if (filter.near.lat && filter.near.lon) { + filterText += ` AND _geoRadius(${filter.near.lat}, ${filter.near.lon}, ${filter.near.radius})` + } + const indexResponse = await ms + .index("trails") + .search(filter.q, { filter: filterText }); + const trailIds = indexResponse.hits.map((h) => h.id); + + if (trailIds.length == 0) { + trails.set([]); + return []; + } + + const dbResponse: Trail[] = (await pb.collection('trails').getList(1, 5, { + filter: trailIds.map((id) => `id="${id}"`).join('||'), expand: "category,waypoints,summit_logs", + sort: `${filter.sortOrder}${filter.sort}` + })).items + + for (const trail of dbResponse) { + setFileURLs(trail); + } + + trails.set(dbResponse); + + return dbResponse; +} + export async function trails_show(id: string, loadGPX?: boolean) { const response: Trail = await pb.collection('trails').getOne(id, { expand: "category,waypoints,summit_logs" }) @@ -248,7 +284,9 @@ function index_trail(trail: Trail) { "distance": trail.distance, "elevation_gain": trail.elevation_gain, "duration": trail.duration, - "category": trail.expand.category?.name, + "category": trail.expand.category?.id, + "completed": trail.expand.summit_logs?.length > 0, + "created": trail.created, "_geo": { "lat": trail.lat, "lng": trail.lon, diff --git a/web/src/lib/util/format_util.ts b/web/src/lib/util/format_util.ts index 460a2497..719af8b2 100644 --- a/web/src/lib/util/format_util.ts +++ b/web/src/lib/util/format_util.ts @@ -15,7 +15,7 @@ export function formatMeters(meters?: number) { return "-"; } if (meters % 1 === 0) { - return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`; + return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${meters} m`; } else { return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${meters.toFixed(2)} m` } diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 7cb76575..ec084f33 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -6,30 +6,15 @@ import CategoryCard from "$lib/components/category_card.svelte"; import TrailCard from "$lib/components/trail/trail_card.svelte"; import { ms } from "$lib/meilisearch"; - import type { Trail } from "$lib/models/trail"; import { categories } from "$lib/stores/category_store"; import { - trails, - trails_delete, - trails_index, + trails } from "$lib/stores/trail_store"; import { currentUser } from "$lib/stores/user_store"; import { country_codes } from "$lib/util/country_code_util"; let searchDropdownItems: SearchItem[] = []; - async function handleDropdownClick( - currentTrail: Trail, - item: { text: string; value: any }, - ) { - if (item.value == "edit") { - goto(`/trail/edit/${currentTrail.id}`); - } else if (item.value == "delete") { - await trails_delete(currentTrail); - await trails_index(); - } - } - async function search(q: string) { const response = await ms.multiSearch({ queries: [ @@ -93,7 +78,6 @@ handleDropdownClick(trail, e.detail)} > {/each} diff --git a/web/src/routes/trails/+page.svelte b/web/src/routes/trails/+page.svelte index 89b5135e..6945217d 100644 --- a/web/src/routes/trails/+page.svelte +++ b/web/src/routes/trails/+page.svelte @@ -6,31 +6,46 @@ import Search, { type SearchItem, } from "$lib/components/base/search.svelte"; + import Select, { + type SelectItem, + } from "$lib/components/base/select.svelte"; import Slider from "$lib/components/base/slider.svelte"; - import TextField from "$lib/components/base/text_field.svelte"; import TrailCard from "$lib/components/trail/trail_card.svelte"; + import TrailListItem from "$lib/components/trail/trail_list_item.svelte"; import { ms } from "$lib/meilisearch"; + import type { Category } from "$lib/models/category"; import type { TrailFilter } from "$lib/models/trail"; import { categories } from "$lib/stores/category_store"; - import { trails } from "$lib/stores/trail_store"; + import { trails, trails_search } from "$lib/stores/trail_store"; import { country_codes } from "$lib/util/country_code_util"; import { formatMeters } from "$lib/util/format_util"; + import { onMount } from "svelte"; - $: maxDistance = Math.max(...$trails.map((t) => t.distance ?? 0)); - $: maxElevationGain = Math.max( - ...$trails.map((t) => t.elevation_gain ?? 0), + $: minDistance = Math.floor( + Math.min(...$trails.map((t) => t.distance ?? 0)), + ); + $: maxDistance = Math.ceil( + Math.max(...$trails.map((t) => t.distance ?? 0)), + ); + $: minElevationGain = Math.floor( + Math.min(...$trails.map((t) => t.elevation_gain ?? 0)), + ); + $: maxElevationGain = Math.ceil( + Math.max(...$trails.map((t) => t.elevation_gain ?? 0)), ); const filter: TrailFilter = { q: "", category: [], near: { - distance: 2000, + radius: 2000, }, - distanceMin: 0, + distanceMin: minDistance, distanceMax: maxDistance, - eleavationGainMin: 0, + eleavationGainMin: minElevationGain, elevationGainMax: maxElevationGain, + sort: "created", + sortOrder: "+", }; const radioGroupItems: RadioItem[] = [ @@ -39,10 +54,47 @@ { text: "No preference", value: "no_preference" }, ]; + const displayOptions: SelectItem[] = [ + { text: "Cards", value: "cards" }, + { text: "List", value: "list" }, + ]; + let selectedDisplayOption = displayOptions[0].value; + + const sortOptions: SelectItem[] = [ + { text: "Alphabetical", value: "name" }, + { text: "Creation date", value: "created" }, + { text: "Distance", value: "distance" }, + { text: "Elevation gain", value: "elevation_gain" }, + ]; + let searchDropdownItems: SearchItem[] = []; let citySearchQuery: string = ""; + onMount(() => { + const storedDisplayOption = localStorage.getItem("displayOption") + if(storedDisplayOption) { + selectedDisplayOption = storedDisplayOption; + } + }) + + async function searchTrails() { + await trails_search(filter); + } + + function setCategoryFilter(category: Category) { + const categoryIndex = filter.category.findIndex( + (c) => c == category.id, + ); + if (categoryIndex !== -1) { + filter.category.splice(categoryIndex, 1); + } else { + filter.category.push(category.id); + } + + searchTrails(); + } + function setCompletedFilter(item: RadioItem) { switch (item.value) { case "no_preference": @@ -58,14 +110,16 @@ filter.completed = undefined; break; } + + searchTrails(); } - async function searchCities(q: string) { + async function searchCities(q: string) { if (q.length == 0) { filter.near.lat = undefined; filter.near.lon = undefined; - console.log(filter.near); - + searchTrails(); + return; } const result = await ms.index("cities500").search(q, { limit: 5 }); @@ -82,21 +136,50 @@ citySearchQuery = item.text; filter.near.lat = item.value.lat; filter.near.lon = item.value.lon; + + searchTrails(); + } + + function setSort() { + searchTrails(); + } + + function setSortOrder() { + if (filter.sortOrder === "+") { + filter.sortOrder = "-"; + } else { + filter.sortOrder = "+"; + } + const sortOrderButton = document.getElementById("sort-order-btn"); + sortOrderButton?.classList.toggle("rotated"); + + searchTrails(); + } + + function setDisplayOption() { + localStorage.setItem("displayOption", selectedDisplayOption); } -
    +
    - +

    Category

    - {#each $categories as category} + {#each $categories as category, i}
    setCategoryFilter(category)} />
    - searchTrails()} >

    Radius: - {formatMeters(filter.near.distance)} + {formatMeters(filter.near.radius)}


    Distance

    searchTrails()} >
    {formatMeters(filter.distanceMin)} @@ -135,9 +224,11 @@

    Elevation Gain

    searchTrails()} >
    {formatMeters(filter.eleavationGainMin)} @@ -152,11 +243,54 @@ on:change={(e) => setCompletedFilter(e.detail)} >
    -
    - {#each $trails as trail} - - - {/each} +
    +
    +
    +

    Sort

    +
    + + +
    +
    +
    +

    Display as

    + + +
    +
    + +
    + +