diff --git a/web/src/css/components.css b/web/src/css/components.css index 9f009483..a255d0ee 100644 --- a/web/src/css/components.css +++ b/web/src/css/components.css @@ -27,19 +27,15 @@ } .tab { - @apply p-4 rounded-t-lg + @apply p-4 rounded-t-lg border border-transparent } .tab:hover { - @apply bg-menu-item-background-hover -} - -.tab-active { - @apply bg-primary text-white + @apply border-menu-item-background-hover } .tab-active:hover { - @apply bg-primary + @apply bg-transparent } .tooltip { diff --git a/web/src/lib/components/base/radio_group.svelte b/web/src/lib/components/base/radio_group.svelte index 1b4241e1..32ceb617 100644 --- a/web/src/lib/components/base/radio_group.svelte +++ b/web/src/lib/components/base/radio_group.svelte @@ -29,7 +29,7 @@ type="radio" checked={i == selected} value={item.value} - class="w-4 h-4 accent-input-background border-input-border focus:ring-input-ring focus:ring-2" + class="w-4 h-4 accent-primary border-input-border focus:ring-input-ring focus:ring-2" on:change={() => handleRadioChange(i)} /> diff --git a/web/src/lib/components/logo/logo_text_two_line.svelte b/web/src/lib/components/logo/logo_text_two_line.svelte deleted file mode 100644 index e26f7c26..00000000 --- a/web/src/lib/components/logo/logo_text_two_line.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/src/lib/components/logo/logo_text_two_line_dark.svelte b/web/src/lib/components/logo/logo_text_two_line_dark.svelte new file mode 100644 index 00000000..77019c98 --- /dev/null +++ b/web/src/lib/components/logo/logo_text_two_line_dark.svelte @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/lib/components/logo/logo_text_two_line_light.svelte b/web/src/lib/components/logo/logo_text_two_line_light.svelte new file mode 100644 index 00000000..9070b37d --- /dev/null +++ b/web/src/lib/components/logo/logo_text_two_line_light.svelte @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/lib/components/nav_bar.svelte b/web/src/lib/components/nav_bar.svelte index d663363c..ad5cdabd 100644 --- a/web/src/lib/components/nav_bar.svelte +++ b/web/src/lib/components/nav_bar.svelte @@ -12,8 +12,7 @@ { text: "Home", value: "/" }, { text: "Trails", value: "/trails" }, { text: "Map", value: "/map" }, - { text: "Category", value: "/" }, - { text: "Favorites", value: "/" }, + { text: "Lists", value: "/lists" }, ]; const dropdownItems = [ @@ -72,7 +71,7 @@ function handleDropdownClick(item: { text: string; value: any }) { if (item.value == "logout") { logout(); - goto("/login"); + goto("/"); } } @@ -85,21 +84,24 @@ {/if} + {#if $currentUser} - -
- {#each navBarItems as item} - {item.text} - {/each} -
-
+ {/if} diff --git a/web/src/lib/components/scene.svelte b/web/src/lib/components/scene.svelte index 2fc4a218..97b60753 100644 --- a/web/src/lib/components/scene.svelte +++ b/web/src/lib/components/scene.svelte @@ -44,7 +44,7 @@ makeDefault position={[5, 2, 0]} on:create={({ ref }) => { - ref.lookAt(0, 0, -0.5); + ref.lookAt(0, 0, 0); }} /> diff --git a/web/src/lib/components/tabs.svelte b/web/src/lib/components/tabs.svelte index af0e402c..fa9ad0e4 100644 --- a/web/src/lib/components/tabs.svelte +++ b/web/src/lib/components/tabs.svelte @@ -1,10 +1,48 @@ -
+
+
{#each tabs as tab, i} - + {/each}
diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index 9c31cd21..636025be 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -1,37 +1,9 @@
-
-

{trail.name}

- {#if $currentUser && $currentUser.id == trail.author && mode == "edit"} - handleDropdownClick(trail, e.detail)} - items={dropdownItems} - > - {/if} -
+

{trail.name}

{trail.location}
-
+
{formatMeters( trail.distance, diff --git a/web/src/lib/components/trail/trail_filter_panel.svelte b/web/src/lib/components/trail/trail_filter_panel.svelte new file mode 100644 index 00000000..df84c141 --- /dev/null +++ b/web/src/lib/components/trail/trail_filter_panel.svelte @@ -0,0 +1,204 @@ + + +
+ {#if showTrailSearch} +
+
+ +
+ +
+ {/if} + + {#if filterExpanded} +
+ {#if showTrailSearch} +
+ {/if} +

Category

+ {#each categories as category, i} +
+ setCategoryFilter(category)} + /> + +
+ {/each} +
+ {#if showCitySearch} +

Near

+
+ searchCities(e.detail)} + on:click={(e) => handleSearchClick(e.detail)} + > +
+ update()} + > +

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

+
+ {/if} +

Distance

+ update()} + > +
+ {formatMeters(filter.distanceMin)} + {formatMeters(filter.distanceMax)} +
+
+

Elevation Gain

+ update()} + > +
+ {formatMeters(filter.elevationGainMin)} + {formatMeters(filter.elevationGainMax)} +
+
+

Completed

+ setCompletedFilter(e.detail)} + > +
+ {/if} +
diff --git a/web/src/lib/components/trail/trail_list_item.svelte b/web/src/lib/components/trail/trail_list_item.svelte index 83db92db..5430996a 100644 --- a/web/src/lib/components/trail/trail_list_item.svelte +++ b/web/src/lib/components/trail/trail_list_item.svelte @@ -1,30 +1,8 @@
  • -
    -

    {trail.name}

    - - {#if $currentUser && $currentUser.id == trail.author} - handleDropdownClick(trail, e.detail)} - items={dropdownItems} - > - {/if} -
    +

    {trail.name}

    {trail.location}
    = 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 +export async function trails_index(data: { perPage: number } = { perPage: 5 }) { + const response: Trail[] = (await pb.collection('trails').getList(1, data.perPage, { expand: "category,waypoints,summit_logs" })).items for (const trail of response) { setFileURLs(trail); @@ -28,7 +28,7 @@ export async function trails_index() { } export async function trails_search_filter(filter: TrailFilter) { - let filterText: string = `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.eleavationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`; + let filterText: string = `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.elevationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`; if (filter.category.length > 0) { filterText += ` AND category IN [${filter.category.join(",")}]`; @@ -63,17 +63,33 @@ export async function trails_search_filter(filter: TrailFilter) { return dbResponse; } -export async function trails_search_bounding_box(northEast: LatLng, southWest: LatLng) { +export async function trails_search_bounding_box(northEast: LatLng, southWest: LatLng, filter?: TrailFilter) { + + let filterText: string = ""; + + if (filter) { + filterText += `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.elevationGainMin} 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}`; + } + } + const response = await ms.index("trails").search("", { filter: [ `_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`, + filterText ], }); const trailIds = response.hits.map((h) => h.id); if (trailIds.length == 0) { + const currentTrails : Trail[] = get(trails); trails.set([]); - return compareObjectArrays(get(trails), []); + return compareObjectArrays(currentTrails, []); } const dbResponse: Trail[] = ( @@ -89,7 +105,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L const gpxData: string = await fetchGPX(trail); trail.expand.gpx_data = gpxData; } - + const comparison = compareObjectArrays(get(trails), dbResponse) trails.set(dbResponse); @@ -175,7 +191,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, formData: ...addedWaypoint, marker: undefined, }); - formData.append("waypoints+", model.id!); + formData.append("waypoints", model.id!); } for (const updatedWaypoint of waypointUpdates.updated) { @@ -183,27 +199,37 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, formData: ...updatedWaypoint, marker: undefined, }); + formData.append("waypoints", model.id!); } for (const deletedWaypoint of waypointUpdates.deleted) { const success = await waypoints_delete(deletedWaypoint.id!); } - const summitLogUpdates = compareObjectArrays(oldTrail.expand.summit_logs ?? [], newTrail.expand.summit_logs ?? []); + for (const unchangedWaypoint of waypointUpdates.unchanged) { + formData.append("waypoints", unchangedWaypoint.id!); + } + + const summitLogUpdates = compareObjectArrays(oldTrail.expand.summit_logs ?? [], newTrail.expand.summit_logs ?? []); for (const summitLog of summitLogUpdates.added) { const model = await summit_logs_create(summitLog); - formData.append("summit_logs+", model.id!); + formData.append("summit_logs", model.id!); } for (const updatedSummitLog of summitLogUpdates.updated) { const model = await summit_logs_update(updatedSummitLog); + formData.append("summit_logs", model.id!); } for (const deletedSummitLog of summitLogUpdates.deleted) { const success = await summit_logs_delete(deletedSummitLog.id!); } + for (const unchangedSummitLog of summitLogUpdates.unchanged) { + formData.append("summit_logs", unchangedSummitLog.id!); + } + for (const file of newTrail._photoFiles) { formData.append("photos", file); } @@ -283,15 +309,18 @@ function setFileURLs(trail: Trail) { } } -function compareObjectArrays(oldArray: T[], newArray: T[]) { +function compareObjectArrays(oldArray: T[], newArray: T[]) { const newObjects = []; const updatedObjects = []; + const unchangedObjects = []; for (const newObj of newArray) { const oldObj = oldArray.find(oldObj => oldObj.id === newObj.id) if (!oldObj) { newObjects.push(newObj); } else if (!util.deepEqual(newObj, oldObj)) { updatedObjects.push(newObj); + } else { + unchangedObjects.push(newObj); } } const deletedObjects = oldArray.filter(oldObj => !newArray.find(newObj => newObj.id === oldObj.id)); @@ -299,29 +328,7 @@ function compareObjectArrays(oldArray: T[], newArray: return { added: newObjects, deleted: deletedObjects, - updated: updatedObjects + updated: updatedObjects, + unchanged: unchangedObjects, }; } - -function index_trail(trail: Trail) { - ms.index('trails').addDocuments([ - { - "id": trail.id, - "author": trail.author, - "name": trail.name, - "description": trail.description, - "location": trail.location, - "distance": trail.distance, - "elevation_gain": trail.elevation_gain, - "duration": trail.duration, - "category": trail.expand.category?.id, - "completed": trail.expand.summit_logs?.length > 0, - "created": trail.created, - "public": trail.public, - "_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 719af8b2..ab28723c 100644 --- a/web/src/lib/util/format_util.ts +++ b/web/src/lib/util/format_util.ts @@ -7,7 +7,7 @@ export function formatTimeHHMM(minutes?: number) { const h = (minutes - m) / 60; - return (h < 10 ? "0" : "") + h.toString() + "h " + (m < 10 ? "0" : "") + m.toString() + "m"; + return (h < 10 ? "0" : "") + h.toString() + "h " + (m < 10 ? "0" : "") + Math.round(m).toString() + "m"; } export function formatMeters(meters?: number) { @@ -15,9 +15,9 @@ export function formatMeters(meters?: number) { return "-"; } if (meters % 1 === 0) { - return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${meters} m`; + return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`; } else { - return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${meters.toFixed(2)} m` + return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${Math.round(meters)} m` } } diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index a06fae03..891ea812 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -60,7 +60,7 @@
    -
    +
    {#each $trails as trail} - {/each}
    diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index 04c515e4..d0084cc4 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -3,6 +3,6 @@ import { trails_index } from "$lib/stores/trail_store"; import type { ServerLoad } from "@sveltejs/kit"; export const load: ServerLoad = async ({ params, locals }) => { - await trails_index() + await trails_index({perPage: 4}) await categories_index() }; \ No newline at end of file diff --git a/web/src/routes/login/+page.svelte b/web/src/routes/login/+page.svelte index 8a5f0740..a4e35aa5 100644 --- a/web/src/routes/login/+page.svelte +++ b/web/src/routes/login/+page.svelte @@ -2,12 +2,15 @@ import { goto } from "$app/navigation"; import Button from "$lib/components/base/button.svelte"; import TextField from "$lib/components/base/text_field.svelte"; - import LogoTextTwoLine from "$lib/components/logo/logo_text_two_line.svelte"; + import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte"; + import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte"; import { show_toast } from "$lib/stores/toast_store"; import { login, type User } from "$lib/stores/user_store"; import { createForm } from "$lib/vendor/svelte-form-lib"; import { ClientResponseError } from "pocketbase"; import { object, string } from "yup"; + import { theme } from "$lib/stores/theme_store"; + let loading: boolean = false; const { form, errors, handleChange, handleSubmit } = createForm({ @@ -50,10 +53,14 @@
    Distance @@ -202,18 +214,21 @@ {/if}
    {#if $trail.tags && $trail.tags.length > 0} -
    +
    {#each $trail.tags as tag} {tag} {/each}
    -
    +
    {/if}
    -
    -
    +
    +
    {#if activeTab == 0}
    {$trail.description} @@ -253,10 +268,19 @@ {/if}
    -
    +
    +
    +
    + +
    +
    +
    - import DoubleSlider from "$lib/components/base/double_slider.svelte"; - import RadioGroup, { - type RadioItem, - } from "$lib/components/base/radio_group.svelte"; - 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 EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte"; import TrailCard from "$lib/components/trail/trail_card.svelte"; + import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.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, trails_search_filter } 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"; - $: 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: { - radius: 2000, - }, - distanceMin: minDistance, - distanceMax: maxDistance, - eleavationGainMin: minElevationGain, - elevationGainMax: maxElevationGain, - sort: "created", - sortOrder: "+", - }; - - const radioGroupItems: RadioItem[] = [ - { text: "Completed", value: "completed" }, - { text: "Not completed", value: "not_completed" }, - { text: "No preference", value: "no_preference" }, - ]; - const displayOptions: SelectItem[] = [ { text: "Cards", value: "cards" }, { text: "List", value: "list" }, @@ -68,12 +24,22 @@ { text: "Elevation gain", value: "elevation_gain" }, ]; - let searchDropdownItems: SearchItem[] = []; - - let citySearchQuery: string = ""; - let filterExpanded: boolean = true; + export const filter: TrailFilter = { + q: "", + category: [], + near: { + radius: 2000, + }, + distanceMin: 0, + distanceMax: 20000, + elevationGainMin: 0, + elevationGainMax: 4000, + sort: "created", + sortOrder: "+", + }; + onMount(() => { const storedDisplayOption = localStorage.getItem("displayOption"); if (storedDisplayOption) { @@ -85,182 +51,38 @@ } }); - async function searchTrails() { + function setDisplayOption() { + localStorage.setItem("displayOption", selectedDisplayOption); + } + + async function setSort() { await trails_search_filter(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": - filter.completed = undefined; - break; - case "completed": - filter.completed = true; - break; - case "not_completed": - filter.completed = false; - break; - default: - filter.completed = undefined; - break; - } - - searchTrails(); - } - - async function searchCities(q: string) { - if (q.length == 0) { - filter.near.lat = undefined; - filter.near.lon = undefined; - searchTrails(); - - return; - } - const result = await ms.index("cities500").search(q, { limit: 5 }); - searchDropdownItems = result.hits.map((h) => ({ - text: h.name, - description: - country_codes[h["country code"] as keyof typeof country_codes], - value: h, - icon: "city", - })); - } - - function handleSearchClick(item: SearchItem) { - citySearchQuery = item.text; - filter.near.lat = item.value.lat; - filter.near.lon = item.value.lon; - - searchTrails(); - } - - function setSort() { - searchTrails(); - } - - function setSortOrder() { + async function setSortOrder() { if (filter.sortOrder === "+") { filter.sortOrder = "-"; } else { filter.sortOrder = "+"; } - searchTrails(); + await trails_search_filter(filter); } - function setDisplayOption() { - localStorage.setItem("displayOption", selectedDisplayOption); + async function handleFilterUpdate() { + await trails_search_filter(filter); }
    -
    -
    -
    - -
    - -
    - - {#if filterExpanded} -
    -

    Category

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

    Near

    -
    - searchCities(e.detail)} - on:click={(e) => handleSearchClick(e.detail)} - > -
    - searchTrails()} - > -

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

    -
    -

    Distance

    - searchTrails()} - > -
    - {formatMeters(filter.distanceMin)} - {formatMeters(filter.distanceMax)} -
    -
    -

    Elevation Gain

    - searchTrails()} - > -
    - {formatMeters(filter.eleavationGainMin)} - {formatMeters(filter.elevationGainMax)} -
    -
    -

    Completed

    - setCompletedFilter(e.detail)} - > - {/if} -
    + handleFilterUpdate()} + >
    @@ -297,8 +119,7 @@ > {#if $trails.length == 0}
    - - +
    {/if} {#each $trails as trail} @@ -308,7 +129,7 @@ href="/trail/view/{trail.id}" > {#if selectedDisplayOption === "cards"} - + {:else} {/if}