adds trail edit
This commit is contained in:
27
web/src/lib/components/base/toast.svelte
Normal file
27
web/src/lib/components/base/toast.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from "$lib/stores/toast_store";
|
||||||
|
|
||||||
|
$: getBackgroundColor = () => {
|
||||||
|
if ($toast && $toast.type == "success") {
|
||||||
|
return "green-200";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
$: getBorderColor = () => {
|
||||||
|
if ($toast && $toast.type == "success") {
|
||||||
|
return "green-500";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $toast}
|
||||||
|
<div
|
||||||
|
class="fixed px-8 py-4 shadow-xl rounded-xl border bottom-4 right-4 bg-{getBackgroundColor()} border-{getBorderColor()}"
|
||||||
|
style="z-index: 1000;"
|
||||||
|
>
|
||||||
|
<i class="fa fa-{$toast.icon} mr-2"></i>
|
||||||
|
<span>{$toast.text}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -12,7 +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">
|
||||||
<a class="btn-primary btn-large" href="/new"><i class="fa fa-plus mr-2"></i>New Trail</a>
|
<a class="btn-primary btn-large" href="/trail/edit/new"><i class="fa fa-plus mr-2"></i>New Trail</a>
|
||||||
<Avatar></Avatar>
|
<Avatar></Avatar>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
const { form, errors, handleChange, handleSubmit } = createForm<SummitLog>({
|
const { form, errors, handleChange, handleSubmit } = createForm<SummitLog>({
|
||||||
initialValues: $summitLog,
|
initialValues: $summitLog,
|
||||||
validationSchema: summitLogSchema,
|
validationSchema: summitLogSchema,
|
||||||
onSubmit: async (submittedValues) => {
|
onSubmit: async (submittedValues) => {
|
||||||
dispatch("save", submittedValues);
|
dispatch("save", submittedValues);
|
||||||
closeModal!();
|
closeModal!();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<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";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import Dropdown from "../base/dropdown.svelte";
|
import Dropdown from "../base/dropdown.svelte";
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@
|
|||||||
|
|
||||||
<div class="trail-card rounded-2xl shadow-md w-72 cursor-pointer">
|
<div class="trail-card rounded-2xl shadow-md w-72 cursor-pointer">
|
||||||
<div class="w-full h-48 overflow-hidden rounded-t-2xl">
|
<div class="w-full h-48 overflow-hidden rounded-t-2xl">
|
||||||
<img src={getFileURL(trail, trail.thumbnail)} alt="" />
|
<img src={trail.thumbnail} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class Trail {
|
|||||||
category?: Category;
|
category?: Category;
|
||||||
waypoints: Waypoint[]
|
waypoints: Waypoint[]
|
||||||
summit_logs: SummitLog[]
|
summit_logs: SummitLog[]
|
||||||
|
gpx_data?: string
|
||||||
}
|
}
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ export async function categories_index() {
|
|||||||
const response: Category[] = (await pb.collection('categories').getFullList<Category>())
|
const response: Category[] = (await pb.collection('categories').getFullList<Category>())
|
||||||
|
|
||||||
categories.set(response);
|
categories.set(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
10
web/src/lib/stores/toast_store.ts
Normal file
10
web/src/lib/stores/toast_store.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export type Toast = {
|
||||||
|
type: "info" | "success" | "warning" | "error"
|
||||||
|
icon: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const toast: Writable<Toast> = writable();
|
||||||
@@ -2,14 +2,26 @@ import { pb } from "$lib/constants";
|
|||||||
import { 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";
|
||||||
|
import { waypoints_create, waypoints_delete } from "./waypoint_store";
|
||||||
|
import { summit_logs_create, summit_logs_delete } from "./summit_log_store";
|
||||||
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
|
import type { SummitLog } from "$lib/models/summit_log";
|
||||||
|
|
||||||
export const trails: Writable<Trail[]> = writable([])
|
export const trails: Writable<Trail[]> = writable([])
|
||||||
export const trail: Writable<Trail> = writable(new Trail(""));
|
export const trail: Writable<Trail> = writable(new Trail(""));
|
||||||
|
|
||||||
|
export const editTrail: 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
|
||||||
|
|
||||||
|
for (const trail of response) {
|
||||||
|
setFileURLs(trail);
|
||||||
|
}
|
||||||
|
|
||||||
trails.set(response);
|
trails.set(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trails_show(id: string, loadGPX?: boolean) {
|
export async function trails_show(id: string, loadGPX?: boolean) {
|
||||||
@@ -17,26 +29,119 @@ export async function trails_show(id: string, loadGPX?: boolean) {
|
|||||||
|
|
||||||
if (loadGPX) {
|
if (loadGPX) {
|
||||||
const gpxData: string = await fetchGPX(response);
|
const gpxData: string = await fetchGPX(response);
|
||||||
response.gpx = gpxData;
|
response.expand.gpx_data = gpxData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFileURLs(response);
|
||||||
|
|
||||||
|
response.expand.waypoints = response.expand.waypoints || [];
|
||||||
|
response.expand.summit_logs = response.expand.summit_logs || [];
|
||||||
|
|
||||||
|
response._photoFiles = [];
|
||||||
|
|
||||||
trail.set(response);
|
trail.set(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trails_create(bodyParams?: { [key: string]: any; } | FormData) {
|
export async function trails_create(trail: Trail, formData: { [key: string]: any; } | FormData) {
|
||||||
const model = await pb
|
formData.set("category", trail.expand.category!.id);
|
||||||
|
|
||||||
|
for (const file of trail._photoFiles) {
|
||||||
|
formData.append("photos", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const waypoint of trail.expand.waypoints) {
|
||||||
|
const model = await waypoints_create({
|
||||||
|
...waypoint,
|
||||||
|
marker: undefined,
|
||||||
|
});
|
||||||
|
formData.append("waypoints", model.id!);
|
||||||
|
}
|
||||||
|
for (const summitLog of trail.expand.summit_logs) {
|
||||||
|
const model = await summit_logs_create(summitLog);
|
||||||
|
formData.append("summit_logs", model.id!);
|
||||||
|
}
|
||||||
|
let model = await pb
|
||||||
.collection("trails")
|
.collection("trails")
|
||||||
.create<Trail>(bodyParams);
|
.create<Trail>(formData);
|
||||||
|
|
||||||
|
const thumbnailIndex = trail.photos.findIndex(
|
||||||
|
(p) => p == trail.thumbnail,
|
||||||
|
);
|
||||||
|
let thumbnail: string | undefined = "/imgs/thumbnail.jpg";
|
||||||
|
if (thumbnailIndex >= 0) {
|
||||||
|
thumbnail = model.photos.at(thumbnailIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
model = await pb
|
||||||
|
.collection("trails")
|
||||||
|
.update<Trail>(model.id!, { thumbnail: thumbnail });
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trails_update(id: string, bodyParams?: { [key: string]: any; } | FormData) {
|
export async function trails_update(oldTrail: Trail, newTrail: Trail, formData: { [key: string]: any; } | FormData) {
|
||||||
const model = await pb
|
|
||||||
.collection("trails")
|
|
||||||
.update<Trail>(id, bodyParams);
|
|
||||||
|
|
||||||
return model;
|
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand.waypoints ?? [], newTrail.expand.waypoints ?? []);
|
||||||
|
|
||||||
|
for (const addedWaypoint of waypointUpdates.added) {
|
||||||
|
const model = await waypoints_create({
|
||||||
|
...addedWaypoint,
|
||||||
|
marker: undefined,
|
||||||
|
});
|
||||||
|
formData.append("waypoints+", model.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deletedWaypoint of waypointUpdates.deleted) {
|
||||||
|
const success = await waypoints_delete(deletedWaypoint.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summitLogUpdates = compareObjectArrays<SummitLog>(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!);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deletedSummitLog of summitLogUpdates.deleted) {
|
||||||
|
const success = await summit_logs_delete(deletedSummitLog.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of newTrail._photoFiles) {
|
||||||
|
formData.append("photos", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedPhotos = oldTrail.photos.filter(oldPhoto => !newTrail.photos.find(newPhoto => newPhoto === oldPhoto));
|
||||||
|
|
||||||
|
for (const deletedPhoto of deletedPhotos) {
|
||||||
|
formData.append("photos-", deletedPhoto.replace(/^.*[\\/]/, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.get("gpx").size == 0) {
|
||||||
|
formData.delete("gpx");
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailIndex = newTrail.photos.findIndex(
|
||||||
|
(p) => p == newTrail.thumbnail,
|
||||||
|
);
|
||||||
|
|
||||||
|
let model = await pb
|
||||||
|
.collection("trails")
|
||||||
|
.update<Trail>(newTrail.id!, formData);
|
||||||
|
|
||||||
|
let thumbnail: string | undefined = oldTrail.thumbnail;
|
||||||
|
if (thumbnailIndex >= 0) {
|
||||||
|
thumbnail = model.photos.at(thumbnailIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
model = await pb
|
||||||
|
.collection("trails")
|
||||||
|
.update<Trail>(model.id!, { thumbnail: thumbnail });
|
||||||
|
|
||||||
|
trail.set(model);
|
||||||
|
|
||||||
|
// return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -57,4 +162,23 @@ async function fetchGPX(trail: Trail) {
|
|||||||
const gpxData = await response.text();
|
const gpxData = await response.text();
|
||||||
|
|
||||||
return gpxData
|
return gpxData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFileURLs(trail: Trail) {
|
||||||
|
trail.thumbnail = getFileURL(trail, trail.thumbnail);
|
||||||
|
for (let i = 0; i < trail.photos.length; i++) {
|
||||||
|
const photo = trail.photos[i];
|
||||||
|
trail.photos[i] = getFileURL(trail, photo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareObjectArrays<T extends { id?: string }>(oldArray: T[], newArray: T[]) {
|
||||||
|
const newObjects = newArray.filter(newObj => !oldArray.find(oldObj => oldObj.id === newObj.id));
|
||||||
|
const deletedObjects = oldArray.filter(oldObj => !newArray.find(newObj => newObj.id === oldObj.id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
added: newObjects,
|
||||||
|
deleted: deletedObjects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import Toast from "$lib/components/base/toast.svelte";
|
||||||
import Footer from "$lib/components/footer.svelte";
|
import Footer from "$lib/components/footer.svelte";
|
||||||
import NavBar from "$lib/components/nav_bar.svelte";
|
import NavBar from "$lib/components/nav_bar.svelte";
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
@@ -7,5 +8,6 @@ import NavBar from "$lib/components/nav_bar.svelte";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NavBar></NavBar>
|
<NavBar></NavBar>
|
||||||
|
<Toast></Toast>
|
||||||
<slot />
|
<slot />
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import CategoryCard from "$lib/components/category_card.svelte";
|
import CategoryCard from "$lib/components/category_card.svelte";
|
||||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||||
import { pb } from "$lib/constants";
|
|
||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import { summit_logs_delete } from "$lib/stores/summit_log_store";
|
import { summit_logs_delete } from "$lib/stores/summit_log_store";
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
currentTrail: Trail,
|
currentTrail: Trail,
|
||||||
item: { text: string; value: any },
|
item: { text: string; value: any },
|
||||||
) {
|
) {
|
||||||
if (item.value == "delete") {
|
if (item.value == "edit") {
|
||||||
|
goto(`/trail/edit/${currentTrail.id}`);
|
||||||
|
} else if (item.value == "delete") {
|
||||||
if (currentTrail.expand.waypoints) {
|
if (currentTrail.expand.waypoints) {
|
||||||
for (const waypoint of currentTrail.expand.waypoints) {
|
for (const waypoint of currentTrail.expand.waypoints) {
|
||||||
waypoints_delete(waypoint.id!);
|
waypoints_delete(waypoint.id!);
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
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}
|
{#each $trails as trail}
|
||||||
<a href="/trail/{trail.id}">
|
<a href="/trail/view/{trail.id}">
|
||||||
<TrailCard
|
<TrailCard
|
||||||
{trail}
|
{trail}
|
||||||
on:change={(e) => handleDropdownClick(trail, e.detail)}
|
on:change={(e) => handleDropdownClick(trail, e.detail)}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Trail } from "$lib/models/trail";
|
|
||||||
import { categories_index } from "$lib/stores/category_store";
|
|
||||||
import { trail } from "$lib/stores/trail_store";
|
|
||||||
import type { Load } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export const load: Load = async ({ params }) => {
|
|
||||||
await categories_index()
|
|
||||||
};
|
|
||||||
@@ -8,14 +8,18 @@
|
|||||||
import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte";
|
import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
||||||
import { pb } from "$lib/constants";
|
|
||||||
import { SummitLog } from "$lib/models/summit_log";
|
import { SummitLog } from "$lib/models/summit_log";
|
||||||
import { Trail, trailSchema } from "$lib/models/trail";
|
import { Trail, trailSchema } from "$lib/models/trail";
|
||||||
import { Waypoint } from "$lib/models/waypoint";
|
import { Waypoint } from "$lib/models/waypoint";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import { summitLog, summit_logs_create } from "$lib/stores/summit_log_store";
|
import { summitLog } from "$lib/stores/summit_log_store";
|
||||||
import { trail, trails_create, trails_update } from "$lib/stores/trail_store";
|
import { toast } from "$lib/stores/toast_store";
|
||||||
import { waypoint, waypoints_create } from "$lib/stores/waypoint_store";
|
import {
|
||||||
|
trail,
|
||||||
|
trails_create,
|
||||||
|
trails_update,
|
||||||
|
} from "$lib/stores/trail_store";
|
||||||
|
import { waypoint } from "$lib/stores/waypoint_store";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
@@ -26,6 +30,8 @@
|
|||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
export let data: { trail: Trail };
|
||||||
|
|
||||||
let L: any;
|
let L: any;
|
||||||
let map: Map;
|
let map: Map;
|
||||||
|
|
||||||
@@ -35,46 +41,6 @@
|
|||||||
let openSummitLogModal: () => void;
|
let openSummitLogModal: () => void;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
|
|
||||||
initialValues: new Trail("adsf", { category: $categories[0] }),
|
|
||||||
validationSchema: trailSchema,
|
|
||||||
onSubmit: async (trail) => {
|
|
||||||
loading = true;
|
|
||||||
try {
|
|
||||||
const form = document.getElementById(
|
|
||||||
"trail-form",
|
|
||||||
) as HTMLFormElement;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
formData.set("category", trail.expand.category!.id);
|
|
||||||
|
|
||||||
for (const file of trail._photoFiles) {
|
|
||||||
formData.append("photos", file);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const waypoint of trail.expand.waypoints) {
|
|
||||||
const model = await waypoints_create({...waypoint, marker: undefined});
|
|
||||||
formData.append("waypoints", model.id!);
|
|
||||||
}
|
|
||||||
for (const summitLog of trail.expand.summit_logs) {
|
|
||||||
const model = await summit_logs_create(summitLog);
|
|
||||||
formData.append("summit_logs", model.id!);
|
|
||||||
}
|
|
||||||
const model = await trails_create(formData);
|
|
||||||
|
|
||||||
const thumbnailIndex = trail.photos.findIndex(
|
|
||||||
(p) => p == trail.thumbnail,
|
|
||||||
);
|
|
||||||
let thumbnail: string | undefined = "/imgs/thumbnail.jpg";
|
|
||||||
if (thumbnailIndex >= 0) {
|
|
||||||
thumbnail = model.photos.at(thumbnailIndex);
|
|
||||||
}
|
|
||||||
await trails_update(model.id!, { thumbnail: thumbnail });
|
|
||||||
} catch (e) {
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
L = (await import("leaflet")).default;
|
L = (await import("leaflet")).default;
|
||||||
@@ -86,8 +52,107 @@
|
|||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
attribution: "© OpenStreetMap contributors",
|
attribution: "© OpenStreetMap contributors",
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.trail.expand.gpx_data &&
|
||||||
|
data.trail.expand.gpx_data.length > 0
|
||||||
|
) {
|
||||||
|
addGPXLayer(data.trail.expand.gpx_data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.trail.expand.waypoints?.length > 0) {
|
||||||
|
for (const waypoint of data.trail.expand.waypoints) {
|
||||||
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
|
marker.addTo(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
|
||||||
|
initialValues: data.trail,
|
||||||
|
validationSchema: trailSchema,
|
||||||
|
onSubmit: async (submittedTrail) => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const form = document.getElementById(
|
||||||
|
"trail-form",
|
||||||
|
) as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
if (!submittedTrail.id) {
|
||||||
|
await trails_create(submittedTrail, formData);
|
||||||
|
} else {
|
||||||
|
await trails_update($trail, submittedTrail, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.set({type: "success", "icon": "check", "text": "Trail saved successfully!"})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function addGPXLayer(gpx: string, addWaypoints: boolean = true) {
|
||||||
|
gpxLayer?.remove();
|
||||||
|
gpxLayer = new L.GPX(gpx, {
|
||||||
|
async: true,
|
||||||
|
gpx_options: {
|
||||||
|
parseElements: [
|
||||||
|
"track",
|
||||||
|
"route",
|
||||||
|
...(addWaypoints ? ["waypoint"] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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 },
|
||||||
|
);
|
||||||
|
if (!$form.expand.waypoints) {
|
||||||
|
}
|
||||||
|
$form.expand.waypoints.push(waypoint);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("loaded", function (e: LeafletEvent) {
|
||||||
|
map.fitBounds(e.target.getBounds());
|
||||||
|
$form.distance = e.target.get_distance();
|
||||||
|
$form.elevation_gain = e.target.get_elevation_gain();
|
||||||
|
$form.duration = e.target.get_total_time() / 1000 / 60;
|
||||||
|
})
|
||||||
|
.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
function openFileBrowser() {
|
function openFileBrowser() {
|
||||||
document.getElementById("fileInput")!.click();
|
document.getElementById("fileInput")!.click();
|
||||||
}
|
}
|
||||||
@@ -110,54 +175,7 @@
|
|||||||
|
|
||||||
reader.onload = function (e) {
|
reader.onload = function (e) {
|
||||||
trail.set(new Trail(""));
|
trail.set(new Trail(""));
|
||||||
gpxLayer?.remove();
|
addGPXLayer(e.target?.result as string);
|
||||||
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 },
|
|
||||||
);
|
|
||||||
$form.expand.waypoints.push(waypoint);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on("loaded", function (e: LeafletEvent) {
|
|
||||||
map.fitBounds(e.target.getBounds());
|
|
||||||
$form.distance = e.target.get_distance();
|
|
||||||
$form.elevation_gain = e.target.get_elevation_gain();
|
|
||||||
$form.duration = e.target.get_total_time() / 1000 / 60;
|
|
||||||
})
|
|
||||||
.addTo(map);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +185,7 @@
|
|||||||
|
|
||||||
function handleWaypointMenuClick(
|
function handleWaypointMenuClick(
|
||||||
currentWaypoint: Waypoint,
|
currentWaypoint: Waypoint,
|
||||||
|
index: number,
|
||||||
e: CustomEvent<{ text: string; value: string }>,
|
e: CustomEvent<{ text: string; value: string }>,
|
||||||
) {
|
) {
|
||||||
if (e.detail.value === "edit") {
|
if (e.detail.value === "edit") {
|
||||||
@@ -174,11 +193,8 @@
|
|||||||
openWaypointModal();
|
openWaypointModal();
|
||||||
} else if (e.detail.value === "delete") {
|
} else if (e.detail.value === "delete") {
|
||||||
currentWaypoint.marker?.remove();
|
currentWaypoint.marker?.remove();
|
||||||
$form.expand.waypoints = $form.expand.waypoints.filter(
|
$form.expand.waypoints.splice(index, 1);
|
||||||
(w) =>
|
$form.expand.waypoints = $form.expand.waypoints;
|
||||||
w.lat !== currentWaypoint.lat &&
|
|
||||||
w.lon !== currentWaypoint.lon,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +276,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beforeSummitLogModalOpen() {
|
function beforeSummitLogModalOpen() {
|
||||||
summitLog.set(new SummitLog(new Date().toISOString()));
|
summitLog.set(new SummitLog(format(new Date(), "yyyy-MM-dd")));
|
||||||
openSummitLogModal();
|
openSummitLogModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,9 +360,16 @@
|
|||||||
<TextField
|
<TextField
|
||||||
name="name"
|
name="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
|
on:change={handleChange}
|
||||||
error={$errors.name}
|
error={$errors.name}
|
||||||
bind:value={$form.name}
|
bind:value={$form.name}
|
||||||
></TextField>
|
></TextField>
|
||||||
|
<TextField
|
||||||
|
name="location"
|
||||||
|
label="Location"
|
||||||
|
error={$errors.location}
|
||||||
|
bind:value={$form.location}
|
||||||
|
></TextField>
|
||||||
<Textarea
|
<Textarea
|
||||||
name="description"
|
name="description"
|
||||||
label="Describe your trail"
|
label="Describe your trail"
|
||||||
@@ -361,12 +384,13 @@
|
|||||||
<hr />
|
<hr />
|
||||||
<h3 class="text-xl font-semibold">Waypoints</h3>
|
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $form.expand.waypoints as waypoint, i}
|
{#each $form.expand.waypoints ?? [] as waypoint, i}
|
||||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||||
<WaypointCard
|
<WaypointCard
|
||||||
{waypoint}
|
{waypoint}
|
||||||
mode="edit"
|
mode="edit"
|
||||||
on:change={(e) => handleWaypointMenuClick(waypoint, e)}
|
on:change={(e) =>
|
||||||
|
handleWaypointMenuClick(waypoint, i, e)}
|
||||||
></WaypointCard>
|
></WaypointCard>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -393,7 +417,7 @@
|
|||||||
style="display: none;"
|
style="display: none;"
|
||||||
on:change={handlePhotoSelection}
|
on:change={handlePhotoSelection}
|
||||||
/>
|
/>
|
||||||
{#each $form.photos as photo, i}
|
{#each $form.photos ?? [] as photo, i}
|
||||||
<div class="shrink-0 grow-0 basis-auto">
|
<div class="shrink-0 grow-0 basis-auto">
|
||||||
<PhotoCard
|
<PhotoCard
|
||||||
src={photo}
|
src={photo}
|
||||||
@@ -407,7 +431,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
<h3 class="text-xl font-semibold">Summit Book</h3>
|
<h3 class="text-xl font-semibold">Summit Book</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $form.expand.summit_logs as log, i}
|
{#each $form.expand.summit_logs ?? [] as log, i}
|
||||||
<li>
|
<li>
|
||||||
<SummitLogCard
|
<SummitLogCard
|
||||||
{log}
|
{log}
|
||||||
20
web/src/routes/trail/edit/[id]/+page.ts
Normal file
20
web/src/routes/trail/edit/[id]/+page.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Trail } from "$lib/models/trail";
|
||||||
|
import { categories_index } from "$lib/stores/category_store";
|
||||||
|
import { trail, trails_show } from "$lib/stores/trail_store";
|
||||||
|
import { error, type Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: Load = async ({ params }) => {
|
||||||
|
if (!params.id) {
|
||||||
|
return error(400, "Bad Request")
|
||||||
|
}
|
||||||
|
const categories = await categories_index()
|
||||||
|
|
||||||
|
let trail: Trail;
|
||||||
|
if (params.id === "new") {
|
||||||
|
trail = new Trail("", { category: categories[0] });
|
||||||
|
} else {
|
||||||
|
trail = await trails_show(params.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { trail: trail }
|
||||||
|
};
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import Tabs from "$lib/components/tabs.svelte";
|
import Tabs from "$lib/components/tabs.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint/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 { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
import type { Icon, Marker } from "leaflet";
|
import type { Icon, Marker } from "leaflet";
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
attribution: "© OpenStreetMap contributors",
|
attribution: "© OpenStreetMap contributors",
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
const gpxLayer = new L.GPX($trail.gpx!, {
|
const gpxLayer = new L.GPX($trail.expand.gpx_data!, {
|
||||||
async: true,
|
async: true,
|
||||||
gpx_options: {
|
gpx_options: {
|
||||||
parseElements: ["track"] as any,
|
parseElements: ["track"] as any,
|
||||||
@@ -76,8 +75,8 @@
|
|||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
lightboxDataSource = $trail.photos.map((v) => ({
|
lightboxDataSource = $trail.photos.map((p) => ({
|
||||||
src: getFileURL($trail, v),
|
src: p,
|
||||||
}));
|
}));
|
||||||
lightbox = new PhotoSwipeLightbox({
|
lightbox = new PhotoSwipeLightbox({
|
||||||
dataSource: lightboxDataSource,
|
dataSource: lightboxDataSource,
|
||||||
@@ -118,7 +117,7 @@
|
|||||||
<section class="relative h-80">
|
<section class="relative h-80">
|
||||||
<img
|
<img
|
||||||
class="w-full h-80"
|
class="w-full h-80"
|
||||||
src={getFileURL($trail, $trail.thumbnail)}
|
src={$trail.thumbnail}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -182,7 +181,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if activeTab == 1}
|
{#if activeTab == 1}
|
||||||
<ul>
|
<ul>
|
||||||
{#each $trail.expand.waypoints as waypoint, i}
|
{#each $trail.expand.waypoints ?? [] as waypoint, i}
|
||||||
<li on:mouseenter={() => openMarkerPopup(i)}>
|
<li on:mouseenter={() => openMarkerPopup(i)}>
|
||||||
<WaypointCard {waypoint}></WaypointCard>
|
<WaypointCard {waypoint}></WaypointCard>
|
||||||
</li>
|
</li>
|
||||||
@@ -194,20 +193,20 @@
|
|||||||
id="photo-gallery"
|
id="photo-gallery"
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
||||||
>
|
>
|
||||||
{#each $trail.photos as photo, i}
|
{#each $trail.photos ?? [] as photo, i}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
<img
|
<img
|
||||||
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
||||||
on:click={() => openGallery(i)}
|
on:click={() => openGallery(i)}
|
||||||
src={getFileURL($trail, photo)}
|
src={photo}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if activeTab == 3}
|
{#if activeTab == 3}
|
||||||
{#each $trail.expand.summit_logs as log}
|
{#each $trail.expand.summit_logs ?? [] as log}
|
||||||
<SummitLogCard {log}></SummitLogCard>
|
<SummitLogCard {log}></SummitLogCard>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
Reference in New Issue
Block a user