adds list editor
This commit is contained in:
221
web/src/routes/lists/edit/[id]/+page.svelte
Normal file
221
web/src/routes/lists/edit/[id]/+page.svelte
Normal file
@@ -0,0 +1,221 @@
|
||||
<script lang="ts">
|
||||
import { listSchema, type List } from "$lib/models/list";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
import { page } from "$app/stores";
|
||||
import Button from "$lib/components/base/button.svelte";
|
||||
import Search, {
|
||||
type SearchItem,
|
||||
} from "$lib/components/base/search.svelte";
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import Textarea from "$lib/components/base/textarea.svelte";
|
||||
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
||||
import TrailListItem from "$lib/components/trail/trail_list_item.svelte";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
import { getFileURL } from "$lib/util/file_util.js";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import type { Trail } from "$lib/models/trail.js";
|
||||
|
||||
export let data;
|
||||
|
||||
let previewURL = "";
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<List>({
|
||||
initialValues: data.list!,
|
||||
validationSchema: listSchema,
|
||||
onSubmit: async (submittedList) => {
|
||||
(document.getElementById("avatar") as HTMLInputElement).value = "";
|
||||
},
|
||||
});
|
||||
|
||||
function openAvatarBrowser() {
|
||||
document.getElementById("avatar")!.click();
|
||||
}
|
||||
|
||||
function handleAvatarSelection() {
|
||||
const files = (document.getElementById("avatar") as HTMLInputElement)
|
||||
.files;
|
||||
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
previewURL = URL.createObjectURL(files[0]);
|
||||
}
|
||||
|
||||
async function search(q: string) {
|
||||
const r = await fetch("/api/v1/search/multi", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
queries: [
|
||||
{
|
||||
indexUid: "trails",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await r.json();
|
||||
|
||||
searchDropdownItems = response.results[0].hits.map(
|
||||
(t: Record<string, any>) => ({
|
||||
text: t.name,
|
||||
description: `${t.location ?? "-"}`,
|
||||
value: t.id,
|
||||
icon: "route",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSearchClick(item: SearchItem) {
|
||||
const trail = await trails_show(item.value, true);
|
||||
$form.trails?.push(trail);
|
||||
$form.expand!.trails = [...$form.expand!.trails, trail];
|
||||
}
|
||||
|
||||
function deleteTrail(trail: Trail) {
|
||||
$form.trails?.filter((id) => id !== trail.id);
|
||||
$form.expand!.trails = $form.expand!.trails.filter(
|
||||
(t) => t.id !== trail.id,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
|
||||
<form
|
||||
id="list-form"
|
||||
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<h2 class="text-2xl font-semibold">
|
||||
{$page.params.id === "new" ? $_("new-list") : $_("edit-list")}
|
||||
</h2>
|
||||
<label for="avatar" class="text-sm font-medium block">
|
||||
{$_("avatar")}
|
||||
</label>
|
||||
<input
|
||||
name="avatar"
|
||||
type="file"
|
||||
id="avatar"
|
||||
accept="image/*"
|
||||
style="display: none;"
|
||||
on:change={handleAvatarSelection}
|
||||
/>
|
||||
<div class="flex items-center gap-4">
|
||||
{#if previewURL.length > 0}
|
||||
<img
|
||||
class="w-32 aspect-square rounded-full object-cover border border-gray-100"
|
||||
alt="avatar"
|
||||
src={previewURL}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex items-center justify-center w-32 aspect-square rounded-full object-cover border border-gray-200"
|
||||
>
|
||||
<i class="fa fa-table-list text-5xl"></i>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={openAvatarBrowser}>{$_("change")}...</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
name="name"
|
||||
label={$_("name")}
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
label={$_("description")}
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
on:change={handleChange}
|
||||
></Textarea>
|
||||
<h3 class="text-xl font-semibold">
|
||||
{$_("trail", { values: { n: 2 } })}
|
||||
</h3>
|
||||
<Search
|
||||
on:update={(e) => search(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
placeholder="{$_('search-trails')}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
{#if $form.expand?.trails.length}
|
||||
{#each $form.expand?.trails ?? [] as trail}
|
||||
<div
|
||||
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
class="h-12 w-12 object-cover rounded-xl"
|
||||
src={trail.photos.length
|
||||
? getFileURL(
|
||||
trail,
|
||||
trail.photos[trail.thumbnail],
|
||||
)
|
||||
: "/imgs/default_thumbnail.webp"}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="basis-full">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-semibold text-lg">
|
||||
{trail.name}
|
||||
</h4>
|
||||
<span class="text-sm"
|
||||
><i class="fa fa-gauge mr-2"></i>{$_(
|
||||
trail.difficulty ?? "?",
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-1 gap-4 text-sm text-gray-500">
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"
|
||||
></i>{formatDistance(trail.distance)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"
|
||||
></i>{formatElevation(
|
||||
trail.elevation_gain,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-clock mr-2"
|
||||
></i>{formatTimeHHMM(trail.duration)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon text-red-500"
|
||||
on:click={() => deleteTrail(trail)}
|
||||
><i class="fa fa-trash"></i></button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-center text-sm text-gray-500 my-8"
|
||||
>No routes added</span
|
||||
>
|
||||
{/if}
|
||||
<Button primary={true} large={true} type="submit" extraClasses="mb-2"
|
||||
>{$_("save-list")}</Button
|
||||
>
|
||||
</form>
|
||||
<MapWithElevation trails={$form.expand?.trails ?? []}></MapWithElevation>
|
||||
</main>
|
||||
27
web/src/routes/lists/edit/[id]/+page.ts
Normal file
27
web/src/routes/lists/edit/[id]/+page.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { List } from "$lib/models/list";
|
||||
import { lists_show } from "$lib/stores/list_store";
|
||||
import { error, type Load } from "@sveltejs/kit";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
export const load: Load = async ({ params, fetch, data }) => {
|
||||
if (!params.id) {
|
||||
return error(400, "Bad Request")
|
||||
}
|
||||
|
||||
let list: List;
|
||||
if (params.id === "new") {
|
||||
list = new List("", []);
|
||||
return { list: list }
|
||||
} else {
|
||||
try {
|
||||
list = await lists_show(params.id, fetch);
|
||||
return { list: list }
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof ClientResponseError) {
|
||||
return error(e.status as any, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -18,7 +18,7 @@
|
||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||
<div id="trail-details" class=" sticky top-[62px]">
|
||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||
<MapWithElevation trails={[$trail]} bind:markers></MapWithElevation>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -509,7 +509,7 @@
|
||||
>
|
||||
<div class="basis-full">
|
||||
<MapWithElevation
|
||||
trail={$trail}
|
||||
trails={[$trail]}
|
||||
options={{
|
||||
theme: "gray-theme",
|
||||
slope: false,
|
||||
|
||||
@@ -889,7 +889,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<MapWithElevation
|
||||
trail={$form}
|
||||
trails={[$form]}
|
||||
crosshair={drawingActive}
|
||||
options={{
|
||||
autofitBounds: !drawingActive,
|
||||
|
||||
Reference in New Issue
Block a user