finishes trail create

This commit is contained in:
Christian Beutel
2024-01-29 21:30:54 +01:00
parent 2d4c673060
commit efaf0d4e5c
47 changed files with 1739 additions and 130 deletions

View File

@@ -1,8 +1,37 @@
<script lang="ts">
import CategoryCard from "$lib/components/category_card.svelte";
import TrailCard from "$lib/components/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 { categories } from "$lib/stores/category_store";
import { trails } from "$lib/stores/trail_store";
import { summit_logs_delete } from "$lib/stores/summit_log_store";
import {
trails,
trails_delete,
trails_index,
} from "$lib/stores/trail_store";
import { waypoints_delete } from "$lib/stores/waypoint_store";
async function handleDropdownClick(
currentTrail: Trail,
item: { text: string; value: any },
) {
if (item.value == "delete") {
if (currentTrail.expand.waypoints) {
for (const waypoint of currentTrail.expand.waypoints) {
waypoints_delete(waypoint.id!);
}
}
if (currentTrail.expand.summit_logs) {
for (const summit_log of currentTrail.expand.summit_logs) {
summit_logs_delete(summit_log.id!);
}
}
await trails_delete(currentTrail.id!);
await trails_index();
}
}
</script>
<section class="hero flex justify-center items-center" style="height: 50vh">
@@ -26,7 +55,12 @@
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}"> <TrailCard {trail}></TrailCard></a>
<a href="/trail/{trail.id}">
<TrailCard
{trail}
on:change={(e) => handleDropdownClick(trail, e.detail)}
></TrailCard></a
>
{/each}
</div>
</section>

View File

@@ -1,8 +1,8 @@
import { categories_index } from "$lib/stores/category_store";
import { trails_index } from "$lib/stores/trail_store";
import type { Load } from "@sveltejs/kit";
import type { ServerLoad } from "@sveltejs/kit";
export const load: Load = async ({ params }) => {
export const load: ServerLoad = async ({ params, locals }) => {
await trails_index()
await categories_index()
};

View File

@@ -1,29 +1,80 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
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 { Trail } from "$lib/models/trail";
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 { trail } from "$lib/stores/trail_store";
import { waypoint } from "$lib/stores/waypoint_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 { 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";
import type { W } from "vitest/dist/reporters-rzC174PQ.js";
let L: any;
let map: Map;
let gpxLayer: GPX;
let openWayPointModal: () => void;
let openWaypointModal: () => void;
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;
@@ -50,7 +101,8 @@
return;
}
$trail.gpx = selectedFile?.name;
$form.gpx = selectedFile?.name;
$form.expand.waypoints = [];
var reader = new FileReader();
@@ -96,14 +148,14 @@
e.point._latlng.lng,
{ name: e.point.options.title, marker: e.point },
);
$trail.expand.waypoints.push(waypoint);
$form.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;
$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);
};
@@ -119,10 +171,10 @@
) {
if (e.detail.value === "edit") {
waypoint.set(currentWaypoint);
openWayPointModal();
openWaypointModal();
} else if (e.detail.value === "delete") {
currentWaypoint.marker?.remove();
$trail.expand.waypoints = $trail.expand.waypoints.filter(
$form.expand.waypoints = $form.expand.waypoints.filter(
(w) =>
w.lat !== currentWaypoint.lat &&
w.lon !== currentWaypoint.lon,
@@ -130,39 +182,136 @@
}
}
function findMapCenterAndOpenModal() {
function beforeWaypointModalOpen() {
const mapCenter = map.getCenter();
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
openWayPointModal();
openWaypointModal();
}
function saveWaypoint(e: CustomEvent<Waypoint>) {
const savedWaypoint = e.detail;
let editedWaypointIndex = $form.expand.waypoints.findIndex(
(s) => s.id == savedWaypoint.id,
);
if ($trail.expand.waypoints.includes(savedWaypoint)) {
$trail.expand.waypoints = $trail.expand.waypoints;
savedWaypoint.marker?.remove();
if (editedWaypointIndex >= 0) {
$form.expand.waypoints[editedWaypointIndex].marker?.remove();
$form.expand.waypoints[editedWaypointIndex] = savedWaypoint;
} else {
$trail.expand.waypoints = [
...$trail.expand.waypoints,
savedWaypoint,
];
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(new Date().toISOString()));
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 class="overflow-scroll flex flex-col gap-4 px-8">
<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" on:click={openFileBrowser}
<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}
/>
@@ -171,33 +320,48 @@
<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>
<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($trail.elevation_gain)}</span
>{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($trail.duration)}</span
<span class="font-medium">{formatTimeHHMM($form.duration)}</span
>
<input type="hidden" name="duration" value={$form.duration} />
</div>
</div>
<TextField label="Name*" bind:value={$trail.name}></TextField>
<Textarea label="Describe your trail" bind:value={$trail.description}
<TextField
name="name"
label="Name"
error={$errors.name}
bind:value={$form.name}
></TextField>
<Textarea
name="description"
label="Describe your trail"
bind:value={$form.description}
></Textarea>
<Select
name="category"
label="Category"
bind:value={$trail.expand.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 $trail.expand.waypoints as waypoint, i}
{#each $form.expand.waypoints as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
<WaypointCard
{waypoint}
@@ -207,14 +371,73 @@
</li>
{/each}
</ul>
<button class="btn-secondary" on:click={findMapCenterAndOpenModal}
<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 bind:openModal={openWaypointModal} on:save={saveWaypoint}
></WaypointModal>
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
></SummitLogModal>
<style>
#map,

View File

@@ -1,4 +1,6 @@
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 }) => {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import SummitLogCard from "$lib/components/summit_log_card.svelte";
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
import Tabs from "$lib/components/tabs.svelte";
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import { trail } from "$lib/stores/trail_store";