adds trail edit

This commit is contained in:
Christian Beutel
2024-01-31 21:55:18 +01:00
parent efaf0d4e5c
commit 99f743ec94
15 changed files with 338 additions and 136 deletions

View 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}

View File

@@ -12,7 +12,7 @@
<a class="font-semibold" href="">Favorites</a>
</menu>
<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>
</div>
</nav>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import type { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util";
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
import Dropdown from "../base/dropdown.svelte";
@@ -14,7 +13,7 @@
<div class="trail-card rounded-2xl shadow-md w-72 cursor-pointer">
<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 class="p-4">
<div>

View File

@@ -17,6 +17,7 @@ class Trail {
category?: Category;
waypoints: Waypoint[]
summit_logs: SummitLog[]
gpx_data?: string
}
tags?: string[];
description?: string;

View File

@@ -8,4 +8,6 @@ export async function categories_index() {
const response: Category[] = (await pb.collection('categories').getFullList<Category>())
categories.set(response);
return response;
}

View 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();

View File

@@ -2,14 +2,26 @@ import { pb } from "$lib/constants";
import { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util";
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 trail: Writable<Trail> = writable(new Trail(""));
export const editTrail: Writable<Trail> = writable(new Trail(""));
export async function trails_index() {
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);
return response;
}
export async function trails_show(id: string, loadGPX?: boolean) {
@@ -17,26 +29,119 @@ export async function trails_show(id: string, loadGPX?: boolean) {
if (loadGPX) {
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);
return response;
}
export async function trails_create(bodyParams?: { [key: string]: any; } | FormData) {
const model = await pb
export async function trails_create(trail: Trail, formData: { [key: string]: any; } | FormData) {
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")
.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;
}
export async function trails_update(id: string, bodyParams?: { [key: string]: any; } | FormData) {
const model = await pb
.collection("trails")
.update<Trail>(id, bodyParams);
export async function trails_update(oldTrail: Trail, newTrail: Trail, formData: { [key: string]: any; } | FormData) {
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;
}
@@ -58,3 +163,22 @@ async function fetchGPX(trail: Trail) {
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
};
}

View File

@@ -1,4 +1,5 @@
<script>
import Toast from "$lib/components/base/toast.svelte";
import Footer from "$lib/components/footer.svelte";
import NavBar from "$lib/components/nav_bar.svelte";
import "../app.css";
@@ -7,5 +8,6 @@ import NavBar from "$lib/components/nav_bar.svelte";
</script>
<NavBar></NavBar>
<Toast></Toast>
<slot />
<Footer></Footer>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import CategoryCard from "$lib/components/category_card.svelte";
import TrailCard from "$lib/components/trail/trail_card.svelte";
import { pb } from "$lib/constants";
import type { Trail } from "$lib/models/trail";
import { categories } from "$lib/stores/category_store";
import { summit_logs_delete } from "$lib/stores/summit_log_store";
@@ -16,7 +16,9 @@
currentTrail: Trail,
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) {
for (const waypoint of currentTrail.expand.waypoints) {
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"
>
{#each $trails as trail}
<a href="/trail/{trail.id}">
<a href="/trail/view/{trail.id}">
<TrailCard
{trail}
on:change={(e) => handleDropdownClick(trail, e.detail)}

View File

@@ -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()
};

View File

@@ -8,14 +8,18 @@
import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte";
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
import { pb } from "$lib/constants";
import { SummitLog } from "$lib/models/summit_log";
import { Trail, trailSchema } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { categories } from "$lib/stores/category_store";
import { summitLog, summit_logs_create } from "$lib/stores/summit_log_store";
import { trail, trails_create, trails_update } from "$lib/stores/trail_store";
import { waypoint, waypoints_create } from "$lib/stores/waypoint_store";
import { summitLog } from "$lib/stores/summit_log_store";
import { toast } from "$lib/stores/toast_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 { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import { createForm } from "$lib/vendor/svelte-form-lib";
@@ -26,6 +30,8 @@
import "leaflet/dist/leaflet.css";
import { onMount } from "svelte";
export let data: { trail: Trail };
let L: any;
let map: Map;
@@ -35,46 +41,6 @@
let openSummitLogModal: () => void;
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 () => {
L = (await import("leaflet")).default;
@@ -86,33 +52,58 @@
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).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);
}
}
});
function openFileBrowser() {
document.getElementById("fileInput")!.click();
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);
}
function handleFileSelection() {
const selectedFile = (
document.getElementById("fileInput") as HTMLInputElement
).files?.[0];
if (!selectedFile) {
return;
toast.set({type: "success", "icon": "check", "text": "Trail saved successfully!"})
} catch (e) {
console.error(e);
} finally {
loading = false;
}
},
});
$form.gpx = selectedFile?.name;
$form.expand.waypoints = [];
var reader = new FileReader();
reader.readAsText(selectedFile);
reader.onload = function (e) {
trail.set(new Trail(""));
function addGPXLayer(gpx: string, addWaypoints: boolean = true) {
gpxLayer?.remove();
gpxLayer = new L.GPX(e.target?.result, {
gpxLayer = new L.GPX(gpx, {
async: true,
gpx_options: {
parseElements: [
"track",
"route",
...(addWaypoints ? ["waypoint"] : []),
],
},
marker_options: {
wptIcons: {
"": L.AwesomeMarkers.icon({
@@ -148,6 +139,8 @@
e.point._latlng.lng,
{ name: e.point.options.title, marker: e.point },
);
if (!$form.expand.waypoints) {
}
$form.expand.waypoints.push(waypoint);
}
})
@@ -158,6 +151,31 @@
$form.duration = e.target.get_total_time() / 1000 / 60;
})
.addTo(map);
}
function openFileBrowser() {
document.getElementById("fileInput")!.click();
}
function handleFileSelection() {
const selectedFile = (
document.getElementById("fileInput") as HTMLInputElement
).files?.[0];
if (!selectedFile) {
return;
}
$form.gpx = selectedFile?.name;
$form.expand.waypoints = [];
var reader = new FileReader();
reader.readAsText(selectedFile);
reader.onload = function (e) {
trail.set(new Trail(""));
addGPXLayer(e.target?.result as string);
};
}
@@ -167,6 +185,7 @@
function handleWaypointMenuClick(
currentWaypoint: Waypoint,
index: number,
e: CustomEvent<{ text: string; value: string }>,
) {
if (e.detail.value === "edit") {
@@ -174,11 +193,8 @@
openWaypointModal();
} else if (e.detail.value === "delete") {
currentWaypoint.marker?.remove();
$form.expand.waypoints = $form.expand.waypoints.filter(
(w) =>
w.lat !== currentWaypoint.lat &&
w.lon !== currentWaypoint.lon,
);
$form.expand.waypoints.splice(index, 1);
$form.expand.waypoints = $form.expand.waypoints;
}
}
@@ -260,7 +276,7 @@
}
function beforeSummitLogModalOpen() {
summitLog.set(new SummitLog(new Date().toISOString()));
summitLog.set(new SummitLog(format(new Date(), "yyyy-MM-dd")));
openSummitLogModal();
}
@@ -344,9 +360,16 @@
<TextField
name="name"
label="Name"
on:change={handleChange}
error={$errors.name}
bind:value={$form.name}
></TextField>
<TextField
name="location"
label="Location"
error={$errors.location}
bind:value={$form.location}
></TextField>
<Textarea
name="description"
label="Describe your trail"
@@ -361,12 +384,13 @@
<hr />
<h3 class="text-xl font-semibold">Waypoints</h3>
<ul>
{#each $form.expand.waypoints as waypoint, i}
{#each $form.expand.waypoints ?? [] as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
<WaypointCard
{waypoint}
mode="edit"
on:change={(e) => handleWaypointMenuClick(waypoint, e)}
on:change={(e) =>
handleWaypointMenuClick(waypoint, i, e)}
></WaypointCard>
</li>
{/each}
@@ -393,7 +417,7 @@
style="display: none;"
on:change={handlePhotoSelection}
/>
{#each $form.photos as photo, i}
{#each $form.photos ?? [] as photo, i}
<div class="shrink-0 grow-0 basis-auto">
<PhotoCard
src={photo}
@@ -407,7 +431,7 @@
<hr />
<h3 class="text-xl font-semibold">Summit Book</h3>
<ul>
{#each $form.expand.summit_logs as log, i}
{#each $form.expand.summit_logs ?? [] as log, i}
<li>
<SummitLogCard
{log}

View 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 }
};

View File

@@ -3,7 +3,6 @@
import Tabs from "$lib/components/tabs.svelte";
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import { trail } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util";
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import type { Icon, Marker } from "leaflet";
@@ -34,7 +33,7 @@
attribution: "© OpenStreetMap contributors",
}).addTo(map);
const gpxLayer = new L.GPX($trail.gpx!, {
const gpxLayer = new L.GPX($trail.expand.gpx_data!, {
async: true,
gpx_options: {
parseElements: ["track"] as any,
@@ -76,8 +75,8 @@
markers.push(marker);
}
lightboxDataSource = $trail.photos.map((v) => ({
src: getFileURL($trail, v),
lightboxDataSource = $trail.photos.map((p) => ({
src: p,
}));
lightbox = new PhotoSwipeLightbox({
dataSource: lightboxDataSource,
@@ -118,7 +117,7 @@
<section class="relative h-80">
<img
class="w-full h-80"
src={getFileURL($trail, $trail.thumbnail)}
src={$trail.thumbnail}
alt=""
/>
<div
@@ -182,7 +181,7 @@
{/if}
{#if activeTab == 1}
<ul>
{#each $trail.expand.waypoints as waypoint, i}
{#each $trail.expand.waypoints ?? [] as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(i)}>
<WaypointCard {waypoint}></WaypointCard>
</li>
@@ -194,20 +193,20 @@
id="photo-gallery"
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-no-noninteractive-element-interactions -->
<img
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
on:click={() => openGallery(i)}
src={getFileURL($trail, photo)}
src={photo}
alt=""
/>
{/each}
</div>
{/if}
{#if activeTab == 3}
{#each $trail.expand.summit_logs as log}
{#each $trail.expand.summit_logs ?? [] as log}
<SummitLogCard {log}></SummitLogCard>
{/each}
{/if}