From 2d4c673060f7c0d16b916fd3be56b6ded8bdf77d Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Mon, 29 Jan 2024 00:41:54 +0100 Subject: [PATCH] adds new trail page --- web/package-lock.json | 50 +++- web/package.json | 4 +- web/src/components.css | 10 +- web/src/lib/components/base/dropdown.svelte | 56 +++++ web/src/lib/components/base/modal.svelte | 52 ++++ web/src/lib/components/base/select.svelte | 21 ++ web/src/lib/components/base/text_field.svelte | 33 +++ web/src/lib/components/base/textarea.svelte | 27 +++ web/src/lib/components/category_card.svelte | 19 +- web/src/lib/components/nav_bar.svelte | 4 +- web/src/lib/components/trail_card.svelte | 23 +- .../components/waypoint/waypoint_card.svelte | 28 +++ .../components/waypoint/waypoint_modal.svelte | 87 +++++++ web/src/lib/components/waypoint_card.svelte | 11 - web/src/lib/models/trail.ts | 55 ++++- web/src/lib/models/waypoint.ts | 35 ++- web/src/lib/stores/category_store.ts | 11 + web/src/lib/stores/trail_store.ts | 6 +- web/src/lib/stores/waypoint_store.ts | 4 + web/src/lib/util/file_util.ts | 5 +- web/src/lib/util/format_util.ts | 17 +- web/src/lib/util/leaflet_util.ts | 26 ++ web/src/routes/+page.svelte | 45 +--- web/src/routes/+page.ts | 8 + web/src/routes/new/+page.svelte | 224 ++++++++++++++++++ web/src/routes/new/+page.ts | 6 + web/src/routes/trail/[id]/+page.svelte | 28 +-- web/src/routes/trail/[id]/+page.ts | 3 +- 28 files changed, 783 insertions(+), 115 deletions(-) create mode 100644 web/src/lib/components/base/dropdown.svelte create mode 100644 web/src/lib/components/base/modal.svelte create mode 100644 web/src/lib/components/base/select.svelte create mode 100644 web/src/lib/components/base/text_field.svelte create mode 100644 web/src/lib/components/base/textarea.svelte create mode 100644 web/src/lib/components/waypoint/waypoint_card.svelte create mode 100644 web/src/lib/components/waypoint/waypoint_modal.svelte delete mode 100644 web/src/lib/components/waypoint_card.svelte create mode 100644 web/src/lib/stores/category_store.ts create mode 100644 web/src/lib/stores/waypoint_store.ts create mode 100644 web/src/lib/util/leaflet_util.ts create mode 100644 web/src/routes/+page.ts create mode 100644 web/src/routes/new/+page.svelte create mode 100644 web/src/routes/new/+page.ts diff --git a/web/package-lock.json b/web/package-lock.json index 52cd0195..35d8b5a6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,7 +14,9 @@ "leaflet-gpx": "^1.7.0", "leaflet.awesome-markers": "^2.0.5", "photoswipe": "^5.4.3", - "pocketbase": "^0.21.0" + "pocketbase": "^0.21.0", + "svelte-forms-lib": "^2.0.1", + "yup": "^1.3.3" }, "devDependencies": { "@playwright/test": "^1.28.1", @@ -1406,7 +1408,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -2597,6 +2598,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3125,6 +3131,14 @@ "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" } }, + "node_modules/svelte-forms-lib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/svelte-forms-lib/-/svelte-forms-lib-2.0.1.tgz", + "integrity": "sha512-kwbJ3ynsepsrrJyAMrvSc0Lj/myc9vfI2DL8OKxgArZimrNYsRh1gENYhvrcKEI3BiZrv8q3VFfmGo/GMyk7Zg==", + "dependencies": { + "dequal": "^2.0.2" + } + }, "node_modules/svelte-hmr": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", @@ -3270,6 +3284,11 @@ "node": ">=0.8" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -3316,6 +3335,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3346,6 +3370,17 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -3732,6 +3767,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz", + "integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/web/package.json b/web/package.json index 3e542de1..d5fd10a9 100644 --- a/web/package.json +++ b/web/package.json @@ -37,6 +37,8 @@ "leaflet-gpx": "^1.7.0", "leaflet.awesome-markers": "^2.0.5", "photoswipe": "^5.4.3", - "pocketbase": "^0.21.0" + "pocketbase": "^0.21.0", + "svelte-forms-lib": "^2.0.1", + "yup": "^1.3.3" } } diff --git a/web/src/components.css b/web/src/components.css index ecc46094..fd737969 100644 --- a/web/src/components.css +++ b/web/src/components.css @@ -3,7 +3,15 @@ @import 'tailwindcss/components'; .btn-primary { - @apply text-white rounded-lg px-4 py-3 bg-primary font-semibold + @apply text-white rounded-lg px-4 py-2 bg-primary font-semibold transition-all hover:bg-gray-900 focus:ring-4 ring-gray-400 +} + +.btn-large { + @apply py-3 +} + +.btn-secondary { + @apply rounded-lg px-4 py-2 border border-primary font-medium transition-all hover:bg-gray-100 focus:ring-4 ring-gray-200 } .tab { diff --git a/web/src/lib/components/base/dropdown.svelte b/web/src/lib/components/base/dropdown.svelte new file mode 100644 index 00000000..9abe57ef --- /dev/null +++ b/web/src/lib/components/base/dropdown.svelte @@ -0,0 +1,56 @@ + + + (isOpen = false)} /> + + + + diff --git a/web/src/lib/components/base/modal.svelte b/web/src/lib/components/base/modal.svelte new file mode 100644 index 00000000..729cd7ef --- /dev/null +++ b/web/src/lib/components/base/modal.svelte @@ -0,0 +1,52 @@ + + + + + + diff --git a/web/src/lib/components/base/select.svelte b/web/src/lib/components/base/select.svelte new file mode 100644 index 00000000..720c5692 --- /dev/null +++ b/web/src/lib/components/base/select.svelte @@ -0,0 +1,21 @@ + + +
+ {#if label.length} +

+ {label} +

+ {/if} + +
diff --git a/web/src/lib/components/base/text_field.svelte b/web/src/lib/components/base/text_field.svelte new file mode 100644 index 00000000..6ecdbfd8 --- /dev/null +++ b/web/src/lib/components/base/text_field.svelte @@ -0,0 +1,33 @@ + + +
+ {#if label.length} +

+ {label} +

+ {/if} +
+ {#if icon.length > 0} + + {/if} + 0} + class:bg-red-50={error.length > 0} + type="text" + bind:value + on:change + {placeholder} + /> +
+ + + {error} + +
diff --git a/web/src/lib/components/base/textarea.svelte b/web/src/lib/components/base/textarea.svelte new file mode 100644 index 00000000..608cf3dd --- /dev/null +++ b/web/src/lib/components/base/textarea.svelte @@ -0,0 +1,27 @@ + + +
+ {#if label.length} +

+ {label} +

+ {/if} + +
+ + +
+ +
+ + +
+ diff --git a/web/src/lib/components/waypoint_card.svelte b/web/src/lib/components/waypoint_card.svelte deleted file mode 100644 index 9ef5dbec..00000000 --- a/web/src/lib/components/waypoint_card.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -
-
{waypoint.name}
-

{waypoint.description}

- {waypoint.lat}, {waypoint.lon} -
diff --git a/web/src/lib/models/trail.ts b/web/src/lib/models/trail.ts index 84939541..63f0b278 100644 --- a/web/src/lib/models/trail.ts +++ b/web/src/lib/models/trail.ts @@ -2,23 +2,56 @@ import type { Category } from "./category"; import type { SummitLog } from "./summit_log"; import type { Waypoint } from "./waypoint"; -interface Trail { - id: string; +class Trail { + id?: string; name: string; - location: string; - distance: number; - elevation_gain: number; - duration: number; - thumbnail: string; + location?: string; + distance?: number; + elevation_gain?: number; + duration?: number; + thumbnail?: string; photos: string[]; - gpx: string; + gpx?: string; expand: { - category: Category; + category?: Category; waypoints: Waypoint[] summit_logs: SummitLog[] } tags?: string[]; - description: string; + description?: string; + + constructor(name: string, + id?: string, + location?: string, + distance?: number, + elevation_gain?: number, + duration?: number, + thumbnail?: string, + photos?: string[], + gpx?: string, + category?: Category, + waypoints?: Waypoint[], + summit_logs?: SummitLog[], + tags?: string[], + description?: string + ) { + this.id = id; + this.name = name; + this.location = location; + this.distance = distance; + this.elevation_gain = elevation_gain; + this.duration = duration; + this.thumbnail = thumbnail; + this.photos = photos ?? []; + this.gpx = gpx; + this.expand = { + category: category, + waypoints: waypoints ?? [], + summit_logs: summit_logs ?? [] + } + this.tags = tags ?? [] + this.description = description ?? ""; + } } -export type { Trail } \ No newline at end of file +export { Trail }; diff --git a/web/src/lib/models/waypoint.ts b/web/src/lib/models/waypoint.ts index 34c734d8..8cbc986c 100644 --- a/web/src/lib/models/waypoint.ts +++ b/web/src/lib/models/waypoint.ts @@ -1,10 +1,33 @@ -interface Waypoint { - id: string; - name: string; - description: string; +import type { Marker } from "leaflet"; +import { date, number, object, string } from "yup"; + +class Waypoint { + id?: string; + name?: string; + description?: string; lat: number; lon: number; - icon: string; + icon?: string; + marker?: Marker; + + constructor(lat: number, lon: number, params?: {id?: string, name?: string, description?: string, icon?: string, marker?: Marker}) { + this.id = params?.id; + this.name = params?.name ?? ""; + this.description = params?.description ?? ""; + this.lat = lat; + this.lon = lon; + this.icon = params?.icon ?? "circle"; + this.marker = params?.marker; + } } -export type { Waypoint } \ No newline at end of file +const waypointSchema = object({ + id: string().optional(), + name: string().optional(), + description: string().optional(), + lat: number().min(0).required('Required').typeError('Invalid latitude'), + lon: number().min(0).required('Required').typeError('Invalid longitude'), + icon: string().optional() + }); + +export { Waypoint, waypointSchema } \ No newline at end of file diff --git a/web/src/lib/stores/category_store.ts b/web/src/lib/stores/category_store.ts new file mode 100644 index 00000000..878d5fa7 --- /dev/null +++ b/web/src/lib/stores/category_store.ts @@ -0,0 +1,11 @@ +import { pb } from "$lib/constants"; +import type { Category } from "$lib/models/category"; +import { writable, type Writable } from "svelte/store"; + +export const categories: Writable = writable([]) + +export async function categories_index() { + const response: Category[] = (await pb.collection('categories').getFullList()) + + categories.set(response); +} \ No newline at end of file diff --git a/web/src/lib/stores/trail_store.ts b/web/src/lib/stores/trail_store.ts index 8fa6fd37..c262f170 100644 --- a/web/src/lib/stores/trail_store.ts +++ b/web/src/lib/stores/trail_store.ts @@ -1,10 +1,10 @@ import { pb } from "$lib/constants"; -import type { Trail } from "$lib/models/trail"; +import { Trail } from "$lib/models/trail"; import { getFileURL } from "$lib/util/file_util"; import { writable, type Writable } from "svelte/store"; -export const trails: Writable = writable() -export const trail: Writable = writable(); +export const trails: Writable = writable([]) +export const trail: 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 diff --git a/web/src/lib/stores/waypoint_store.ts b/web/src/lib/stores/waypoint_store.ts new file mode 100644 index 00000000..85236133 --- /dev/null +++ b/web/src/lib/stores/waypoint_store.ts @@ -0,0 +1,4 @@ +import { Waypoint } from "$lib/models/waypoint"; +import { writable, type Writable } from "svelte/store"; + +export const waypoint: Writable = writable(new Waypoint(0, 0)); diff --git a/web/src/lib/util/file_util.ts b/web/src/lib/util/file_util.ts index f685fce7..18b56c7f 100644 --- a/web/src/lib/util/file_util.ts +++ b/web/src/lib/util/file_util.ts @@ -1,6 +1,9 @@ import { pb } from "$lib/constants"; import type { FileOptions } from "pocketbase"; -export function getFileURL(record: { [key: string]: any; }, filename: string, options?: FileOptions) { +export function getFileURL(record: { [key: string]: any; }, filename?: string, options?: FileOptions) { + if(!filename) { + return ""; + } return pb.files.getUrl(record, filename, options); } \ No newline at end of file diff --git a/web/src/lib/util/format_util.ts b/web/src/lib/util/format_util.ts index fe54e144..4e10d8b6 100644 --- a/web/src/lib/util/format_util.ts +++ b/web/src/lib/util/format_util.ts @@ -1,4 +1,9 @@ -export function formatTimeHHMM(minutes: number) { +import type { Waypoint } from "$lib/models/waypoint"; + +export function formatTimeHHMM(minutes?: number) { + if(!minutes) { + return "-"; + } const m = minutes % 60; const h = (minutes - m) / 60; @@ -6,7 +11,10 @@ export function formatTimeHHMM(minutes: number) { return (h < 10 ? "0" : "") + h.toString() + "h " + (m < 10 ? "0" : "") + m.toString() + "m"; } -export function formatMeters(meters: number) { +export function formatMeters(meters?: number) { + if(!meters) { + return "-"; + } if (meters % 1 === 0) { return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`; } else { @@ -15,7 +23,10 @@ export function formatMeters(meters: number) { } -export function formatISODate(isoTimestamp: string) { +export function formatISODate(isoTimestamp?: string) { + if(!isoTimestamp) { + return "-" + } const date = new Date(isoTimestamp); const day = date.getDate().toString().padStart(2, '0'); diff --git a/web/src/lib/util/leaflet_util.ts b/web/src/lib/util/leaflet_util.ts new file mode 100644 index 00000000..90629543 --- /dev/null +++ b/web/src/lib/util/leaflet_util.ts @@ -0,0 +1,26 @@ +import type { Waypoint } from "$lib/models/waypoint"; +import type { Icon, Marker } from "leaflet"; + +export function createMarkerFromWaypoint(L: any, waypoint: Waypoint): Marker { + const fontAwesomeIcon = L.AwesomeMarkers.icon({ + icon: waypoint.icon, + prefix: "fa", + markerColor: "cadetblue", + iconColor: "white", + }) as Icon; + + const marker = L.marker([waypoint.lat, waypoint.lon], { + title: waypoint.name, + icon: fontAwesomeIcon, + }) + .bindPopup( + "" + + waypoint.name + + "" + + (waypoint.description && waypoint.description.length > 0 + ? "
" + waypoint.description + : ""), + ); + + return marker; +} \ No newline at end of file diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index d0fa1147..871b0730 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,43 +1,8 @@
@@ -60,8 +25,8 @@ id="trails" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 justify-items-center gap-8 py-8" > - {#each trails as trail,i} - + {#each $trails as trail} + {/each}
@@ -71,7 +36,7 @@ id="categories" class="grid grid-cols- sm:grid-cols-2 lg:grid-cols-4 justify-items-center gap-8 py-8" > - {#each categories as category} + {#each $categories as category} {/each} diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts new file mode 100644 index 00000000..35061744 --- /dev/null +++ b/web/src/routes/+page.ts @@ -0,0 +1,8 @@ +import { categories_index } from "$lib/stores/category_store"; +import { trails_index } from "$lib/stores/trail_store"; +import type { Load } from "@sveltejs/kit"; + +export const load: Load = async ({ params }) => { + await trails_index() + await categories_index() +}; \ No newline at end of file diff --git a/web/src/routes/new/+page.svelte b/web/src/routes/new/+page.svelte new file mode 100644 index 00000000..bb9c0bd3 --- /dev/null +++ b/web/src/routes/new/+page.svelte @@ -0,0 +1,224 @@ + + +
+
+

Pick a trail

+ + +
+

Basic Info

+
+
+ Distance + {formatMeters($trail.distance)} +
+
+ Elevation gain + {formatMeters($trail.elevation_gain)} +
+
+ Est. duration + {formatTimeHHMM($trail.duration)} +
+
+ + + +
+

Waypoints

+
    + {#each $trail.expand.waypoints as waypoint, i} +
  • openMarkerPopup(waypoint)}> + handleWaypointMenuClick(waypoint, e)} + > +
  • + {/each} +
+ +
+
+
+ + + diff --git a/web/src/routes/new/+page.ts b/web/src/routes/new/+page.ts new file mode 100644 index 00000000..ad170da0 --- /dev/null +++ b/web/src/routes/new/+page.ts @@ -0,0 +1,6 @@ +import { categories_index } from "$lib/stores/category_store"; +import type { Load } from "@sveltejs/kit"; + +export const load: Load = async ({ params }) => { + await categories_index() +}; \ No newline at end of file diff --git a/web/src/routes/trail/[id]/+page.svelte b/web/src/routes/trail/[id]/+page.svelte index 162bc139..b01648de 100644 --- a/web/src/routes/trail/[id]/+page.svelte +++ b/web/src/routes/trail/[id]/+page.svelte @@ -1,10 +1,11 @@