adds trail edit
This commit is contained in:
471
web/src/routes/trail/edit/[id]/+page.svelte
Normal file
471
web/src/routes/trail/edit/[id]/+page.svelte
Normal file
@@ -0,0 +1,471 @@
|
||||
<script lang="ts">
|
||||
import Button from "$lib/components/base/button.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 PhotoCard from "$lib/components/photo_card.svelte";
|
||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||
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 { 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 } 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";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import { format } from "date-fns";
|
||||
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";
|
||||
|
||||
export let data: { trail: Trail };
|
||||
|
||||
let L: any;
|
||||
let map: Map;
|
||||
|
||||
let gpxLayer: GPX;
|
||||
|
||||
let openWaypointModal: () => void;
|
||||
let openSummitLogModal: () => void;
|
||||
|
||||
let loading = false;
|
||||
|
||||
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);
|
||||
|
||||
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() {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
function openMarkerPopup(waypoint: Waypoint) {
|
||||
waypoint.marker?.openPopup();
|
||||
}
|
||||
|
||||
function handleWaypointMenuClick(
|
||||
currentWaypoint: Waypoint,
|
||||
index: number,
|
||||
e: CustomEvent<{ text: string; value: string }>,
|
||||
) {
|
||||
if (e.detail.value === "edit") {
|
||||
waypoint.set(currentWaypoint);
|
||||
openWaypointModal();
|
||||
} else if (e.detail.value === "delete") {
|
||||
currentWaypoint.marker?.remove();
|
||||
$form.expand.waypoints.splice(index, 1);
|
||||
$form.expand.waypoints = $form.expand.waypoints;
|
||||
}
|
||||
}
|
||||
|
||||
function beforeWaypointModalOpen() {
|
||||
const mapCenter = map.getCenter();
|
||||
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
|
||||
openWaypointModal();
|
||||
}
|
||||
|
||||
function saveWaypoint(e: CustomEvent<Waypoint>) {
|
||||
const savedWaypoint = e.detail;
|
||||
let editedWaypointIndex = $form.expand.waypoints.findIndex(
|
||||
(s) => s.id == savedWaypoint.id,
|
||||
);
|
||||
|
||||
if (editedWaypointIndex >= 0) {
|
||||
$form.expand.waypoints[editedWaypointIndex].marker?.remove();
|
||||
$form.expand.waypoints[editedWaypointIndex] = savedWaypoint;
|
||||
} else {
|
||||
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
||||
|
||||
$form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint];
|
||||
}
|
||||
const marker = createMarkerFromWaypoint(L, savedWaypoint);
|
||||
marker.addTo(map);
|
||||
savedWaypoint.marker = marker;
|
||||
}
|
||||
|
||||
function openPhotoBrowser() {
|
||||
document.getElementById("photoInput")!.click();
|
||||
}
|
||||
|
||||
function handlePhotoSelection() {
|
||||
const files = (
|
||||
document.getElementById("photoInput") as HTMLInputElement
|
||||
).files;
|
||||
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
$form._photoFiles.push(file);
|
||||
|
||||
(function (file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
if (e.target?.result) {
|
||||
if ($form.photos.length == 0) {
|
||||
$form.thumbnail = e.target.result as string;
|
||||
}
|
||||
$form.photos = [
|
||||
...$form.photos,
|
||||
e.target.result as string,
|
||||
];
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
})(file);
|
||||
}
|
||||
}
|
||||
|
||||
function makePhotoThumbnail(index: number) {
|
||||
$form.thumbnail = $form.photos[index];
|
||||
}
|
||||
|
||||
function handlePhotoDelete(index: number) {
|
||||
const photoToDelete = $form.photos[index];
|
||||
let reassignThumbnail: boolean = false;
|
||||
if ($form.thumbnail == photoToDelete) {
|
||||
reassignThumbnail = true;
|
||||
}
|
||||
$form.photos.splice(index, 1);
|
||||
$form._photoFiles.splice(index, 1);
|
||||
$form.photos = $form.photos;
|
||||
if (reassignThumbnail) {
|
||||
$form.thumbnail = $form.photos[0];
|
||||
}
|
||||
}
|
||||
|
||||
function beforeSummitLogModalOpen() {
|
||||
summitLog.set(new SummitLog(format(new Date(), "yyyy-MM-dd")));
|
||||
openSummitLogModal();
|
||||
}
|
||||
|
||||
function saveSummitLog(e: CustomEvent<SummitLog>) {
|
||||
const savedSummitLog = e.detail;
|
||||
savedSummitLog.date = format(savedSummitLog.date, "yyyy-MM-dd");
|
||||
let editedSummitLogIndex = $form.expand.summit_logs.findIndex(
|
||||
(s) => s.id == savedSummitLog.id,
|
||||
);
|
||||
|
||||
if (editedSummitLogIndex >= 0) {
|
||||
$form.expand.summit_logs[editedSummitLogIndex] = savedSummitLog;
|
||||
} else {
|
||||
savedSummitLog.id = cryptoRandomString({ length: 15 });
|
||||
$form.expand.summit_logs = [
|
||||
...$form.expand.summit_logs,
|
||||
savedSummitLog,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function handleSummitLogMenuClick(
|
||||
currentSummitLog: SummitLog,
|
||||
index: number,
|
||||
e: CustomEvent<{ text: string; value: string }>,
|
||||
) {
|
||||
if (e.detail.value === "edit") {
|
||||
summitLog.set(currentSummitLog);
|
||||
openSummitLogModal();
|
||||
} else if (e.detail.value === "delete") {
|
||||
$form.expand.summit_logs.splice(index, 1);
|
||||
$form.expand.summit_logs = $form.expand.summit_logs;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="grid grid-cols-[400px_1fr]">
|
||||
<form
|
||||
id="trail-form"
|
||||
class="overflow-y-scroll overflow-x-hidden flex flex-col gap-4 px-8"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<h3 class="text-xl font-semibold">Pick a trail</h3>
|
||||
<button class="btn-primary" type="button" on:click={openFileBrowser}
|
||||
>Upload GPX</button
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="gpx"
|
||||
id="fileInput"
|
||||
accept=".gpx"
|
||||
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($form.distance)}</span>
|
||||
<input type="hidden" name="distance" value={$form.distance} />
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Elevation gain</span>
|
||||
<span class="font-medium"
|
||||
>{formatMeters($form.elevation_gain)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="elevation_gain"
|
||||
value={$form.elevation_gain}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Est. duration</span>
|
||||
<span class="font-medium">{formatTimeHHMM($form.duration)}</span
|
||||
>
|
||||
<input type="hidden" name="duration" value={$form.duration} />
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
bind:value={$form.description}
|
||||
></Textarea>
|
||||
<Select
|
||||
name="category"
|
||||
label="Category"
|
||||
bind:value={$form.expand.category}
|
||||
items={$categories.map((c) => ({ text: c.name, value: c }))}
|
||||
></Select>
|
||||
<hr />
|
||||
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||
<ul>
|
||||
{#each $form.expand.waypoints ?? [] as waypoint, i}
|
||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||
<WaypointCard
|
||||
{waypoint}
|
||||
mode="edit"
|
||||
on:change={(e) =>
|
||||
handleWaypointMenuClick(waypoint, i, e)}
|
||||
></WaypointCard>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<button
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={beforeWaypointModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>Add Waypoint</button
|
||||
>
|
||||
<hr />
|
||||
<h3 class="text-xl font-semibold">Photos</h3>
|
||||
<div class="flex gap-4 max-w-full overflow-x-scroll shrink-0">
|
||||
<button
|
||||
class="btn-secondary h-32 w-32 shrink-0 grow-0 basis-auto"
|
||||
type="button"
|
||||
on:click={openPhotoBrowser}><i class="fa fa-plus"></i></button
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="photoInput"
|
||||
accept="image/*"
|
||||
multiple={true}
|
||||
style="display: none;"
|
||||
on:change={handlePhotoSelection}
|
||||
/>
|
||||
{#each $form.photos ?? [] as photo, i}
|
||||
<div class="shrink-0 grow-0 basis-auto">
|
||||
<PhotoCard
|
||||
src={photo}
|
||||
on:delete={() => handlePhotoDelete(i)}
|
||||
isThumbnail={$form.thumbnail === photo}
|
||||
on:thumbnail={() => makePhotoThumbnail(i)}
|
||||
></PhotoCard>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<hr />
|
||||
<h3 class="text-xl font-semibold">Summit Book</h3>
|
||||
<ul>
|
||||
{#each $form.expand.summit_logs ?? [] as log, i}
|
||||
<li>
|
||||
<SummitLogCard
|
||||
{log}
|
||||
mode="edit"
|
||||
on:change={(e) => handleSummitLogMenuClick(log, i, e)}
|
||||
></SummitLogCard>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<button
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={beforeSummitLogModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>Add Entry</button
|
||||
>
|
||||
<hr />
|
||||
<Button
|
||||
primary={true}
|
||||
large={true}
|
||||
type="submit"
|
||||
extraClasses="mb-2"
|
||||
{loading}>Save Trail</Button
|
||||
>
|
||||
</form>
|
||||
<div class="rounded-xl" id="map"></div>
|
||||
</main>
|
||||
<WaypointModal bind:openModal={openWaypointModal} on:save={saveWaypoint}
|
||||
></WaypointModal>
|
||||
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
|
||||
></SummitLogModal>
|
||||
|
||||
<style>
|
||||
#map,
|
||||
form {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user