adds new trail page
This commit is contained in:
50
web/package-lock.json
generated
50
web/package-lock.json
generated
@@ -14,7 +14,9 @@
|
|||||||
"leaflet-gpx": "^1.7.0",
|
"leaflet-gpx": "^1.7.0",
|
||||||
"leaflet.awesome-markers": "^2.0.5",
|
"leaflet.awesome-markers": "^2.0.5",
|
||||||
"photoswipe": "^5.4.3",
|
"photoswipe": "^5.4.3",
|
||||||
"pocketbase": "^0.21.0"
|
"pocketbase": "^0.21.0",
|
||||||
|
"svelte-forms-lib": "^2.0.1",
|
||||||
|
"yup": "^1.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
@@ -1406,7 +1408,6 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -2597,6 +2598,11 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"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": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"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"
|
"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": {
|
"node_modules/svelte-hmr": {
|
||||||
"version": "0.15.3",
|
"version": "0.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||||
@@ -3270,6 +3284,11 @@
|
|||||||
"node": ">=0.8"
|
"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": {
|
"node_modules/tiny-glob": {
|
||||||
"version": "0.2.9",
|
"version": "0.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||||
@@ -3316,6 +3335,11 @@
|
|||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/totalist": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
@@ -3346,6 +3370,17 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||||
@@ -3732,6 +3767,17 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
"leaflet-gpx": "^1.7.0",
|
"leaflet-gpx": "^1.7.0",
|
||||||
"leaflet.awesome-markers": "^2.0.5",
|
"leaflet.awesome-markers": "^2.0.5",
|
||||||
"photoswipe": "^5.4.3",
|
"photoswipe": "^5.4.3",
|
||||||
"pocketbase": "^0.21.0"
|
"pocketbase": "^0.21.0",
|
||||||
|
"svelte-forms-lib": "^2.0.1",
|
||||||
|
"yup": "^1.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
@import 'tailwindcss/components';
|
@import 'tailwindcss/components';
|
||||||
|
|
||||||
.btn-primary {
|
.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 {
|
.tab {
|
||||||
|
|||||||
56
web/src/lib/components/base/dropdown.svelte
Normal file
56
web/src/lib/components/base/dropdown.svelte
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let items: { text: string; value: any }[] = [];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemClick(item: { text: string, value: any }) {
|
||||||
|
dispatch("change", item);
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:mouseup={() => (isOpen = false)} />
|
||||||
|
|
||||||
|
<div class="dropdown relative">
|
||||||
|
<button
|
||||||
|
class="hover:bg-gray-100 w-8 h-8 rounded-full"
|
||||||
|
on:click={toggleMenu}
|
||||||
|
>
|
||||||
|
<i class="fa fa-ellipsis-vertical"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<ul
|
||||||
|
class="menu absolute bg-white border rounded-l-xl rounded-b-xl shadow-md z-10 right-0 overflow-hidden"
|
||||||
|
class:none={isOpen}
|
||||||
|
>
|
||||||
|
{#each items as item}
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
<li
|
||||||
|
class="menu-item p-4 cursor-pointer hover:bg-gray-100 focus:bg-gray-200 transition-colors"
|
||||||
|
tabindex="0"
|
||||||
|
on:mouseup={() => handleItemClick(item)}
|
||||||
|
on:keydown={() => handleItemClick(item)}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
52
web/src/lib/components/base/modal.svelte
Normal file
52
web/src/lib/components/base/modal.svelte
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let id: string;
|
||||||
|
export let title: string;
|
||||||
|
export function openModal() {
|
||||||
|
(document.getElementById(id) as HTMLDialogElement).showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeModal() {
|
||||||
|
(document.getElementById(id) as HTMLDialogElement).close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot {openModal} />
|
||||||
|
<dialog
|
||||||
|
{id}
|
||||||
|
tabindex="-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="relative w-full max-w-2xl max-h-full rounded-xl"
|
||||||
|
>
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="relative bg-white shadow dark:bg-gray-700">
|
||||||
|
<!-- Modal header -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
on:click={closeModal}
|
||||||
|
>
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
<span class="sr-only">Close modal</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Modal body -->
|
||||||
|
<div class="p-4 md:p-5 space-y-4">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
<!-- Modal footer -->
|
||||||
|
<div
|
||||||
|
class="p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<slot name="footer" {closeModal} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
21
web/src/lib/components/base/select.svelte
Normal file
21
web/src/lib/components/base/select.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value: any;
|
||||||
|
export let items: { text: string; value: any }[] = [];
|
||||||
|
export let label: string = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if label.length}
|
||||||
|
<p class="text-sm font-medium pb-1">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<select
|
||||||
|
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
|
||||||
|
>
|
||||||
|
{#each items as item}
|
||||||
|
<option value={item.value}>{item.text}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
33
web/src/lib/components/base/text_field.svelte
Normal file
33
web/src/lib/components/base/text_field.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value: string | number = "";
|
||||||
|
export let placeholder: string = "";
|
||||||
|
export let label: string = "";
|
||||||
|
export let error: string = "";
|
||||||
|
export let icon: string = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if label.length}
|
||||||
|
<p class="text-sm font-medium pb-1">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if icon.length > 0}
|
||||||
|
<i class="fa fa-{icon}"></i>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
class="bg-gray-50 border rounded-md p-3 transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||||
|
class:border-red-400={error.length > 0}
|
||||||
|
class:bg-red-50={error.length > 0}
|
||||||
|
type="text"
|
||||||
|
bind:value
|
||||||
|
on:change
|
||||||
|
{placeholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="textfield-error text-xs text-red-400">
|
||||||
|
{error}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
27
web/src/lib/components/base/textarea.svelte
Normal file
27
web/src/lib/components/base/textarea.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value: string | number = "";
|
||||||
|
export let placeholder: string = "";
|
||||||
|
export let rows: number = 3;
|
||||||
|
export let label: string = "";
|
||||||
|
export let error: string = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if label.length}
|
||||||
|
<p class="text-sm font-medium mb-1">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<textarea
|
||||||
|
class="bg-gray-50 border rounded-md p-3 resize-none transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||||
|
{rows}
|
||||||
|
{placeholder}
|
||||||
|
class:border-red-400={error.length > 0}
|
||||||
|
class:bg-red-50={error.length > 0}
|
||||||
|
bind:value
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
<span class="textfield-error text-xs text-red-400">
|
||||||
|
{error}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Category } from "$lib/models/category";
|
import type { Category } from "$lib/models/category";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
|
||||||
export let category: Category;
|
export let category: Category;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="category-card relative rounded-2xl shadow-md w-48 h-48 overflow-hidden cursor-pointer">
|
<div
|
||||||
<img class="w-full h-full" src={category.img} alt="" />
|
class="category-card relative rounded-2xl shadow-md w-48 h-48 overflow-hidden cursor-pointer"
|
||||||
<div class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"></div>
|
>
|
||||||
<h5 class="absolute text-white font-bold bottom-4 left-4 text-xl">{category.name}</h5>
|
<img
|
||||||
|
class="w-full h-full"
|
||||||
|
src={getFileURL(category, category.img)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||||
|
></div>
|
||||||
|
<h5 class="absolute text-white font-bold bottom-4 left-4 text-xl">
|
||||||
|
{category.name}
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -12,9 +12,7 @@
|
|||||||
<a class="font-semibold" href="">Favorites</a>
|
<a class="font-semibold" href="">Favorites</a>
|
||||||
</menu>
|
</menu>
|
||||||
<div class="flex gap-6 items-center">
|
<div class="flex gap-6 items-center">
|
||||||
<button class="btn-primary"
|
<a class="btn-primary btn-large" href="/new"><i class="fa fa-plus mr-2"></i>New Trail</a>
|
||||||
><i class="fa fa-plus mr-2"></i>New Trail</button
|
|
||||||
>
|
|
||||||
<Avatar></Avatar>
|
<Avatar></Avatar>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
|
|
||||||
export let trail: Trail;
|
export let trail: Trail;
|
||||||
</script>
|
</script>
|
||||||
@@ -7,15 +9,28 @@
|
|||||||
<div
|
<div
|
||||||
class="trail-card rounded-2xl shadow-md w-72 overflow-hidden cursor-pointer"
|
class="trail-card rounded-2xl shadow-md w-72 overflow-hidden cursor-pointer"
|
||||||
>
|
>
|
||||||
<img class="w-full h-48" src={trail.img} alt="" />
|
<img class="w-full h-48" src={getFileURL(trail, trail.thumbnail)} alt="" />
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-lg">{trail.name}</h4>
|
<h4 class="font-semibold text-lg">{trail.name}</h4>
|
||||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 gap-4">
|
<div class="flex mt-2 gap-4 text-sm text-gray-500">
|
||||||
<span><i class="fa fa-left-right mr-2"></i>{trail.distance}m</span>
|
<span
|
||||||
<span><i class="fa fa-up-down mr-2"></i>{trail.height}m</span>
|
><i class="fa fa-left-right mr-2"></i>{formatMeters(
|
||||||
|
trail.distance,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-up-down mr-2"></i>{formatMeters(
|
||||||
|
trail.elevation_gain,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||||
|
trail.duration,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
web/src/lib/components/waypoint/waypoint_card.svelte
Normal file
28
web/src/lib/components/waypoint/waypoint_card.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
|
import Dropdown from "../base/dropdown.svelte";
|
||||||
|
|
||||||
|
export let waypoint: Waypoint;
|
||||||
|
export let mode: "show" | "edit" = "show";
|
||||||
|
|
||||||
|
const dropdownItems = [
|
||||||
|
{ text: "Edit", value: "edit" },
|
||||||
|
{ text: "Delete", value: "delete" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4 border rounded-md my-2 hover:bg-gray-50">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h5>
|
||||||
|
<i class="fa fa-{waypoint.icon} mr-2"></i>{waypoint.name}
|
||||||
|
</h5>
|
||||||
|
{#if mode == "edit"}
|
||||||
|
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if waypoint.description}
|
||||||
|
<p>{waypoint.description}</p>
|
||||||
|
{/if}
|
||||||
|
<span class="text-sm text-gray-500">{waypoint.lat}, {waypoint.lon}</span>
|
||||||
|
</div>
|
||||||
87
web/src/lib/components/waypoint/waypoint_modal.svelte
Normal file
87
web/src/lib/components/waypoint/waypoint_modal.svelte
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import { createForm } from "svelte-forms-lib";
|
||||||
|
import Modal from "../base/modal.svelte";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
import Textarea from "../base/textarea.svelte";
|
||||||
|
import { waypoint } from "$lib/stores/waypoint_store";
|
||||||
|
|
||||||
|
export let openModal: (() => void) | undefined = undefined;
|
||||||
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<Waypoint>({
|
||||||
|
initialValues: $waypoint,
|
||||||
|
validationSchema: waypointSchema,
|
||||||
|
onSubmit: async (submittedWaypoint) => {
|
||||||
|
dispatch("save", submittedWaypoint);
|
||||||
|
closeModal!();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$: form.set($waypoint);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="waypoint-modal"
|
||||||
|
title="Add Waypoint"
|
||||||
|
let:openModal
|
||||||
|
bind:openModal
|
||||||
|
bind:closeModal
|
||||||
|
>
|
||||||
|
<slot {openModal} />
|
||||||
|
<form
|
||||||
|
id="waypoint-form"
|
||||||
|
slot="content"
|
||||||
|
class="modal-content space-y-4"
|
||||||
|
on:submit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="basis-full">
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
bind:value={$form.name}
|
||||||
|
error={$errors.name}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Icon"
|
||||||
|
bind:value={$form.icon}
|
||||||
|
icon={$form.icon}
|
||||||
|
error={$errors.icon}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Description"
|
||||||
|
bind:value={$form.description}
|
||||||
|
error={$errors.description}
|
||||||
|
on:change={handleChange}
|
||||||
|
></Textarea>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<TextField
|
||||||
|
label="Latitude"
|
||||||
|
bind:value={$form.lat}
|
||||||
|
error={$errors.lat}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
<TextField
|
||||||
|
label="Longitude"
|
||||||
|
bind:value={$form.lon}
|
||||||
|
error={$errors.lat}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div slot="footer" class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||||
|
<button class="btn-primary" type="submit" form="waypoint-form"
|
||||||
|
>Save</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Waypoint } from "$lib/models/waypoint";
|
|
||||||
|
|
||||||
export let waypoint: Waypoint;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="p-4 border rounded-md my-2 cursor-pointer hover:bg-gray-100">
|
|
||||||
<h5 class="mb-2"><i class="fa fa-{waypoint.icon} mr-2"></i>{waypoint.name}</h5>
|
|
||||||
<p>{waypoint.description}</p>
|
|
||||||
<span class="text-sm text-gray-500">{waypoint.lat}, {waypoint.lon}</span>
|
|
||||||
</div>
|
|
||||||
@@ -2,23 +2,56 @@ import type { Category } from "./category";
|
|||||||
import type { SummitLog } from "./summit_log";
|
import type { SummitLog } from "./summit_log";
|
||||||
import type { Waypoint } from "./waypoint";
|
import type { Waypoint } from "./waypoint";
|
||||||
|
|
||||||
interface Trail {
|
class Trail {
|
||||||
id: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
location?: string;
|
||||||
distance: number;
|
distance?: number;
|
||||||
elevation_gain: number;
|
elevation_gain?: number;
|
||||||
duration: number;
|
duration?: number;
|
||||||
thumbnail: string;
|
thumbnail?: string;
|
||||||
photos: string[];
|
photos: string[];
|
||||||
gpx: string;
|
gpx?: string;
|
||||||
expand: {
|
expand: {
|
||||||
category: Category;
|
category?: Category;
|
||||||
waypoints: Waypoint[]
|
waypoints: Waypoint[]
|
||||||
summit_logs: SummitLog[]
|
summit_logs: SummitLog[]
|
||||||
}
|
}
|
||||||
tags?: string[];
|
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 }
|
export { Trail };
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
interface Waypoint {
|
import type { Marker } from "leaflet";
|
||||||
id: string;
|
import { date, number, object, string } from "yup";
|
||||||
name: string;
|
|
||||||
description: string;
|
class Waypoint {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: 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 }
|
const waypointSchema = object<Waypoint>({
|
||||||
|
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 }
|
||||||
11
web/src/lib/stores/category_store.ts
Normal file
11
web/src/lib/stores/category_store.ts
Normal file
@@ -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<Category[]> = writable([])
|
||||||
|
|
||||||
|
export async function categories_index() {
|
||||||
|
const response: Category[] = (await pb.collection('categories').getFullList<Category>())
|
||||||
|
|
||||||
|
categories.set(response);
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { pb } from "$lib/constants";
|
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 { getFileURL } from "$lib/util/file_util";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
export const trails: Writable<Trail[]> = writable()
|
export const trails: Writable<Trail[]> = writable([])
|
||||||
export const trail: Writable<Trail> = writable();
|
export const trail: Writable<Trail> = writable(new Trail(""));
|
||||||
|
|
||||||
export async function trails_index() {
|
export async function trails_index() {
|
||||||
const response: Trail[] = (await pb.collection('trails').getList<Trail>(1, 5, { expand: "category,waypoints,summit_logs" })).items
|
const response: Trail[] = (await pb.collection('trails').getList<Trail>(1, 5, { expand: "category,waypoints,summit_logs" })).items
|
||||||
|
|||||||
4
web/src/lib/stores/waypoint_store.ts
Normal file
4
web/src/lib/stores/waypoint_store.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Waypoint } from "$lib/models/waypoint";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const waypoint: Writable<Waypoint> = writable(new Waypoint(0, 0));
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { pb } from "$lib/constants";
|
import { pb } from "$lib/constants";
|
||||||
import type { FileOptions } from "pocketbase";
|
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);
|
return pb.files.getUrl(record, filename, options);
|
||||||
}
|
}
|
||||||
@@ -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 m = minutes % 60;
|
||||||
|
|
||||||
const h = (minutes - m) / 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";
|
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) {
|
if (meters % 1 === 0) {
|
||||||
return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`;
|
return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`;
|
||||||
} else {
|
} 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 date = new Date(isoTimestamp);
|
||||||
|
|
||||||
const day = date.getDate().toString().padStart(2, '0');
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
|||||||
26
web/src/lib/util/leaflet_util.ts
Normal file
26
web/src/lib/util/leaflet_util.ts
Normal file
@@ -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(
|
||||||
|
"<b>" +
|
||||||
|
waypoint.name +
|
||||||
|
"</b>" +
|
||||||
|
(waypoint.description && waypoint.description.length > 0
|
||||||
|
? "<br>" + waypoint.description
|
||||||
|
: ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
@@ -1,43 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CategoryCard from "$lib/components/category_card.svelte";
|
import CategoryCard from "$lib/components/category_card.svelte";
|
||||||
import TrailCard from "$lib/components/trail_card.svelte";
|
import TrailCard from "$lib/components/trail_card.svelte";
|
||||||
import type { Category } from "$lib/models/category";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import type { Trail } from "$lib/models/trail";
|
import { trails } from "$lib/stores/trail_store";
|
||||||
|
|
||||||
const categories: Category[] = [
|
|
||||||
{
|
|
||||||
name: "Hiking",
|
|
||||||
img: "https://images.unsplash.com/photo-1551632811-561732d1e306?q=80&w=3270&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Walking",
|
|
||||||
img: "https://images.unsplash.com/photo-1606474226448-4aa808468efc?q=80&w=3380&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Biking",
|
|
||||||
img: "https://images.unsplash.com/photo-1629626495452-37bfe96e142b?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Climbing",
|
|
||||||
img: "https://images.unsplash.com/photo-1605548109944-9040d0972bf5?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTF8fGNsaW1iaW5nfGVufDB8fDB8fHww",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Skiing",
|
|
||||||
img: "https://images.unsplash.com/photo-1616429553002-faf23468952d?q=80&w=3270&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const trails: Trail[] = [
|
|
||||||
{
|
|
||||||
name: "Breitenstein",
|
|
||||||
location: "Fischbachau",
|
|
||||||
distance: 4000,
|
|
||||||
height: 1400,
|
|
||||||
img: "https://images.unsplash.com/photo-1570641963303-92ce4845ed4c?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
|
||||||
category: categories[0],
|
|
||||||
description: ""
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="hero flex justify-center items-center" style="height: 50vh">
|
<section class="hero flex justify-center items-center" style="height: 50vh">
|
||||||
@@ -60,8 +25,8 @@
|
|||||||
id="trails"
|
id="trails"
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 justify-items-center gap-8 py-8"
|
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}
|
||||||
<a href="/trail/{i}"> <TrailCard {trail}></TrailCard></a>
|
<a href="/trail/{trail.id}"> <TrailCard {trail}></TrailCard></a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -71,7 +36,7 @@
|
|||||||
id="categories"
|
id="categories"
|
||||||
class="grid grid-cols- sm:grid-cols-2 lg:grid-cols-4 justify-items-center gap-8 py-8"
|
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}
|
||||||
<CategoryCard {category}></CategoryCard>
|
<CategoryCard {category}></CategoryCard>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
8
web/src/routes/+page.ts
Normal file
8
web/src/routes/+page.ts
Normal file
@@ -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()
|
||||||
|
};
|
||||||
224
web/src/routes/new/+page.svelte
Normal file
224
web/src/routes/new/+page.svelte
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import Select from "$lib/components/base/select.svelte";
|
||||||
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import Textarea from "$lib/components/base/textarea.svelte";
|
||||||
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
|
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
||||||
|
import { Trail } from "$lib/models/trail";
|
||||||
|
import { Waypoint } from "$lib/models/waypoint";
|
||||||
|
import { categories } from "$lib/stores/category_store";
|
||||||
|
import { trail } from "$lib/stores/trail_store";
|
||||||
|
import { waypoint } from "$lib/stores/waypoint_store";
|
||||||
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
|
import type { GPX, Icon, LeafletEvent, Map } from "leaflet";
|
||||||
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { W } from "vitest/dist/reporters-rzC174PQ.js";
|
||||||
|
|
||||||
|
let L: any;
|
||||||
|
let map: Map;
|
||||||
|
|
||||||
|
let gpxLayer: GPX;
|
||||||
|
|
||||||
|
let openWayPointModal: () => void;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
L = (await import("leaflet")).default;
|
||||||
|
await import("leaflet-gpx");
|
||||||
|
await import("leaflet.awesome-markers");
|
||||||
|
|
||||||
|
map = L.map("map").setView([0, 0], 2);
|
||||||
|
|
||||||
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
}).addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
function openFileBrowser() {
|
||||||
|
document.getElementById("fileInput")!.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelection() {
|
||||||
|
const selectedFile = (
|
||||||
|
document.getElementById("fileInput") as HTMLInputElement
|
||||||
|
).files?.[0];
|
||||||
|
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trail.gpx = selectedFile?.name;
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
reader.readAsText(selectedFile);
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
trail.set(new Trail(""));
|
||||||
|
gpxLayer?.remove();
|
||||||
|
gpxLayer = new L.GPX(e.target?.result, {
|
||||||
|
async: true,
|
||||||
|
marker_options: {
|
||||||
|
wptIcons: {
|
||||||
|
"": L.AwesomeMarkers.icon({
|
||||||
|
icon: "circle",
|
||||||
|
prefix: "fa",
|
||||||
|
markerColor: "cadetblue",
|
||||||
|
iconColor: "white",
|
||||||
|
}) as Icon,
|
||||||
|
},
|
||||||
|
startIcon: L.AwesomeMarkers.icon({
|
||||||
|
icon: "circle-half-stroke",
|
||||||
|
prefix: "fa",
|
||||||
|
markerColor: "cadetblue",
|
||||||
|
iconColor: "white",
|
||||||
|
}) as Icon,
|
||||||
|
endIcon: L.AwesomeMarkers.icon({
|
||||||
|
icon: "flag-checkered",
|
||||||
|
prefix: "fa",
|
||||||
|
markerColor: "cadetblue",
|
||||||
|
iconColor: "white",
|
||||||
|
}) as Icon,
|
||||||
|
startIconUrl: "",
|
||||||
|
endIconUrl: "",
|
||||||
|
shadowUrl: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.on("addpoint", function (e: any) {
|
||||||
|
if (e.point_type === "start") {
|
||||||
|
e.point.setZIndexOffset(1000);
|
||||||
|
} else if (e.point_type === "waypoint") {
|
||||||
|
const waypoint = new Waypoint(
|
||||||
|
e.point._latlng.lat,
|
||||||
|
e.point._latlng.lng,
|
||||||
|
{ name: e.point.options.title, marker: e.point },
|
||||||
|
);
|
||||||
|
$trail.expand.waypoints.push(waypoint);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("loaded", function (e: LeafletEvent) {
|
||||||
|
map.fitBounds(e.target.getBounds());
|
||||||
|
$trail.distance = e.target.get_distance();
|
||||||
|
$trail.elevation_gain = e.target.get_elevation_gain();
|
||||||
|
$trail.duration = e.target.get_total_time() / 1000 / 60;
|
||||||
|
})
|
||||||
|
.addTo(map);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMarkerPopup(waypoint: Waypoint) {
|
||||||
|
waypoint.marker?.openPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWaypointMenuClick(
|
||||||
|
currentWaypoint: Waypoint,
|
||||||
|
e: CustomEvent<{ text: string; value: string }>,
|
||||||
|
) {
|
||||||
|
if (e.detail.value === "edit") {
|
||||||
|
waypoint.set(currentWaypoint);
|
||||||
|
openWayPointModal();
|
||||||
|
} else if (e.detail.value === "delete") {
|
||||||
|
currentWaypoint.marker?.remove();
|
||||||
|
$trail.expand.waypoints = $trail.expand.waypoints.filter(
|
||||||
|
(w) =>
|
||||||
|
w.lat !== currentWaypoint.lat &&
|
||||||
|
w.lon !== currentWaypoint.lon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMapCenterAndOpenModal() {
|
||||||
|
const mapCenter = map.getCenter();
|
||||||
|
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
|
||||||
|
openWayPointModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveWaypoint(e: CustomEvent<Waypoint>) {
|
||||||
|
const savedWaypoint = e.detail;
|
||||||
|
|
||||||
|
if ($trail.expand.waypoints.includes(savedWaypoint)) {
|
||||||
|
$trail.expand.waypoints = $trail.expand.waypoints;
|
||||||
|
savedWaypoint.marker?.remove();
|
||||||
|
} else {
|
||||||
|
$trail.expand.waypoints = [
|
||||||
|
...$trail.expand.waypoints,
|
||||||
|
savedWaypoint,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const marker = createMarkerFromWaypoint(L, savedWaypoint);
|
||||||
|
marker.addTo(map);
|
||||||
|
savedWaypoint.marker = marker;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="grid grid-cols-[400px_1fr]">
|
||||||
|
<form class="overflow-scroll flex flex-col gap-4 px-8">
|
||||||
|
<h3 class="text-xl font-semibold">Pick a trail</h3>
|
||||||
|
<button class="btn-primary" on:click={openFileBrowser}
|
||||||
|
>Upload GPX</button
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="fileInput"
|
||||||
|
style="display: none;"
|
||||||
|
on:change={handleFileSelection}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<h3 class="text-xl font-semibold">Basic Info</h3>
|
||||||
|
<div class="flex gap-4 justify-around">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span>Distance</span>
|
||||||
|
<span class="font-medium">{formatMeters($trail.distance)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span>Elevation gain</span>
|
||||||
|
<span class="font-medium"
|
||||||
|
>{formatMeters($trail.elevation_gain)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span>Est. duration</span>
|
||||||
|
<span class="font-medium"
|
||||||
|
>{formatTimeHHMM($trail.duration)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextField label="Name*" bind:value={$trail.name}></TextField>
|
||||||
|
<Textarea label="Describe your trail" bind:value={$trail.description}
|
||||||
|
></Textarea>
|
||||||
|
<Select
|
||||||
|
label="Category"
|
||||||
|
bind:value={$trail.expand.category}
|
||||||
|
items={$categories.map((c) => ({ text: c.name, value: c }))}
|
||||||
|
></Select>
|
||||||
|
<hr />
|
||||||
|
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||||
|
<ul>
|
||||||
|
{#each $trail.expand.waypoints as waypoint, i}
|
||||||
|
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||||
|
<WaypointCard
|
||||||
|
{waypoint}
|
||||||
|
mode="edit"
|
||||||
|
on:change={(e) => handleWaypointMenuClick(waypoint, e)}
|
||||||
|
></WaypointCard>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<button class="btn-secondary" on:click={findMapCenterAndOpenModal}
|
||||||
|
><i class="fa fa-plus mr-2"></i>Add Waypoint</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
<div class="rounded-xl" id="map"></div>
|
||||||
|
</main>
|
||||||
|
<WaypointModal bind:openModal={openWayPointModal} on:save={saveWaypoint}
|
||||||
|
></WaypointModal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#map,
|
||||||
|
form {
|
||||||
|
height: calc(100vh - 124px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
web/src/routes/new/+page.ts
Normal file
6
web/src/routes/new/+page.ts
Normal file
@@ -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()
|
||||||
|
};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SummitLogCard from "$lib/components/summit_log_card.svelte";
|
import SummitLogCard from "$lib/components/summit_log_card.svelte";
|
||||||
import Tabs from "$lib/components/tabs.svelte";
|
import Tabs from "$lib/components/tabs.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint_card.svelte";
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
import { trail } from "$lib/stores/trail_store";
|
import { trail } from "$lib/stores/trail_store";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
import type { Icon, Marker } from "leaflet";
|
import type { Icon, Marker } from "leaflet";
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
attribution: "© OpenStreetMap contributors",
|
attribution: "© OpenStreetMap contributors",
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
const gpxLayer = new L.GPX($trail.gpx, {
|
const gpxLayer = new L.GPX($trail.gpx!, {
|
||||||
async: true,
|
async: true,
|
||||||
gpx_options: {
|
gpx_options: {
|
||||||
parseElements: ["track"] as any,
|
parseElements: ["track"] as any,
|
||||||
@@ -70,27 +71,8 @@
|
|||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
for (const waypoint of $trail.expand.waypoints) {
|
for (const waypoint of $trail.expand.waypoints) {
|
||||||
const fontAwesomeIcon = L.AwesomeMarkers.icon({
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
icon: waypoint.icon,
|
marker.addTo(map);
|
||||||
prefix: "fa",
|
|
||||||
markerColor: "cadetblue",
|
|
||||||
iconColor: "white",
|
|
||||||
}) as Icon;
|
|
||||||
|
|
||||||
const marker = L.marker([waypoint.lat, waypoint.lon], {
|
|
||||||
title: waypoint.name,
|
|
||||||
icon: fontAwesomeIcon,
|
|
||||||
})
|
|
||||||
.addTo(map)
|
|
||||||
.bindPopup(
|
|
||||||
"<b>" +
|
|
||||||
waypoint.name +
|
|
||||||
"</b>" +
|
|
||||||
(waypoint.description.length > 0
|
|
||||||
? "<br>" + waypoint.description
|
|
||||||
: ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Load } from "@sveltejs/kit";
|
|
||||||
import type {Trail} from "$lib/models/trail"
|
|
||||||
import { trails_show } from "$lib/stores/trail_store";
|
import { trails_show } from "$lib/stores/trail_store";
|
||||||
|
import type { Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: Load = async ({ params }) => {
|
export const load: Load = async ({ params }) => {
|
||||||
await trails_show(params.id!, true)
|
await trails_show(params.id!, true)
|
||||||
|
|||||||
Reference in New Issue
Block a user