refactor zo zod and felte
This commit is contained in:
76
web/package-lock.json
generated
76
web/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "wanderer",
|
||||
"version": "0.12.0",
|
||||
"dependencies": {
|
||||
"@felte/validator-zod": "^1.0.18",
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@threlte/core": "^7.1.0",
|
||||
@@ -22,6 +23,7 @@
|
||||
"chartjs-plugin-crosshair": "^2.0.0",
|
||||
"chartjs-plugin-zoom": "^2.1.0",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"felte": "^1.3.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
"isomorphic-xml2js": "^0.1.3",
|
||||
@@ -38,7 +40,6 @@
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"three": "^0.161.0",
|
||||
"xmldom": "^0.6.0",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -438,6 +439,39 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@felte/common": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@felte/common/-/common-1.1.9.tgz",
|
||||
"integrity": "sha512-d9e9M5FWc7AyaX/fb2I78AvwIAAA6S8ncKoTd3vYdPGQfgryl18oMU+ScM1WgFnbOBSoItdt61oaZAuRqfE4AA==",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@felte/core": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@felte/core/-/core-1.4.4.tgz",
|
||||
"integrity": "sha512-qXEofmfc0tKvd1MOP9T1ixoI4+u0ZcUtC7JpBfb2A/lr/mhc8HrkvVWGatrGBnNovTc4lgIKWOySVVFswa4BYg==",
|
||||
"dependencies": {
|
||||
"@felte/common": "1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@felte/validator-zod": {
|
||||
"version": "1.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@felte/validator-zod/-/validator-zod-1.0.18.tgz",
|
||||
"integrity": "sha512-nLhy3qhCrL14iMxIHXtIH0MckZr/dmpqRd/KcqQ6HJoT2tjvv8gPpgOGJdOR15Wfzd25n/UUr3tyX7ydgXA5xQ==",
|
||||
"dependencies": {
|
||||
"@felte/common": "1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
|
||||
@@ -2717,6 +2751,20 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/felte": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/felte/-/felte-1.3.0.tgz",
|
||||
"integrity": "sha512-J09vuOC2Hw/d+ajhT4ueVEcSU0af+tlnfwF9QWVTuOWQf286pSGS48cFYhjmS7K7hUNMRUIwXwCnHWpyasadmA==",
|
||||
"dependencies": {
|
||||
"@felte/core": "1.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.31.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||
@@ -4823,11 +4871,6 @@
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
@@ -5866,11 +5909,6 @@
|
||||
"next-tick": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
|
||||
},
|
||||
"node_modules/tiny-glob": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||
@@ -5926,11 +5964,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
@@ -6729,17 +6762,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz",
|
||||
"integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@felte/validator-zod": "^1.0.18",
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@threlte/core": "^7.1.0",
|
||||
@@ -44,6 +45,7 @@
|
||||
"chartjs-plugin-crosshair": "^2.0.0",
|
||||
"chartjs-plugin-zoom": "^2.1.0",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"felte": "^1.3.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
"isomorphic-xml2js": "^0.1.3",
|
||||
@@ -60,7 +62,6 @@
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"three": "^0.161.0",
|
||||
"xmldom": "^0.6.0",
|
||||
"yup": "^1.3.3",
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: string | Date = "";
|
||||
export let label: string = "";
|
||||
export let error: string = "";
|
||||
export let error: string | string[] | null = "";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -15,15 +17,17 @@
|
||||
<input
|
||||
{name}
|
||||
class="bg-input-background border border-input-border rounded-md p-3 transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 w-full"
|
||||
class:border-red-400={error.length > 0}
|
||||
class:bg-red-50={error.length > 0}
|
||||
class:border-red-400={(error?.length ?? 0) > 0}
|
||||
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
||||
type="date"
|
||||
bind:value
|
||||
on:change
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<span class="textfield-error text-xs text-red-400">
|
||||
{error}
|
||||
{error instanceof Array ? $_(error[0]) : error}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: any;
|
||||
export let items: SelectItem[] = [];
|
||||
export let value: any = items.at(0)?.value ?? "";
|
||||
export let label: string = "";
|
||||
export let disabled: boolean = false;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let disabled: boolean = false;
|
||||
export let label: string = "";
|
||||
export let error: string = "";
|
||||
export let error: string | string[] | null = "";
|
||||
export let icon: string = "";
|
||||
export let extraClasses: string = "";
|
||||
export let type: "text" | "password" | "search" = "text";
|
||||
@@ -29,8 +31,8 @@
|
||||
<input
|
||||
{name}
|
||||
class="bg-input-background border border-input-border rounded-md p-3 transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 w-full {extraClasses}"
|
||||
class:border-red-400={error.length > 0}
|
||||
class:bg-input-background-error={error.length > 0}
|
||||
class:border-red-400={(error?.length ?? 0) > 0}
|
||||
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
||||
class:text-gray-500={disabled}
|
||||
{disabled}
|
||||
{autocomplete}
|
||||
@@ -44,7 +46,9 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<span class="textfield-error text-xs text-red-400">
|
||||
{error}
|
||||
{error instanceof Array ? $_(error[0]) : error}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let rows: number = 3;
|
||||
export let label: string = "";
|
||||
export let error: string = "";
|
||||
export let error: string | string[] | null = "";
|
||||
export let extraClasses: string = "";
|
||||
</script>
|
||||
|
||||
@@ -19,12 +21,14 @@
|
||||
class="bg-input-background border border-input-border rounded-md p-3 resize-none transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 w-full {extraClasses}"
|
||||
{rows}
|
||||
{placeholder}
|
||||
class:border-red-400={error.length > 0}
|
||||
class:bg-red-50={error.length > 0}
|
||||
class:border-red-400={(error?.length ?? 0) > 0}
|
||||
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
||||
bind:value
|
||||
on:change
|
||||
/>
|
||||
{#if error}
|
||||
<span class="textfield-error text-xs text-red-400">
|
||||
{error}
|
||||
{error instanceof Array ? $_(error[0]) : error}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -125,13 +125,17 @@
|
||||
{#each { length: 5 } as _, index}
|
||||
<SkeletonNotificationCard></SkeletonNotificationCard>
|
||||
{/each}
|
||||
{:else}
|
||||
{:else if notifications.length}
|
||||
{#each notifications as notification}
|
||||
<NotificationCard
|
||||
on:click={handleNotificationClick}
|
||||
{notification}
|
||||
></NotificationCard>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-gray-500 text-sm text-center py-6">
|
||||
No notifications
|
||||
</p>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Modal from "$lib/components/base/modal.svelte";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { z } from "zod";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { object, string } from "yup";
|
||||
|
||||
let _openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
export function openModal() {
|
||||
$form.email = email
|
||||
$errors.email = "";
|
||||
setFields("email", email);
|
||||
setErrors("email", []);
|
||||
_openModal!();
|
||||
}
|
||||
|
||||
@@ -18,17 +19,20 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<{
|
||||
const { form, errors, setFields, setErrors } = createForm<{
|
||||
email: string;
|
||||
}>({
|
||||
initialValues: { email: email },
|
||||
validationSchema: object({
|
||||
email: string()
|
||||
.email($_("not-a-valid-email-address"))
|
||||
.required($_("required")),
|
||||
extend: validator({
|
||||
schema: z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "required")
|
||||
.email("not-a-valid-email-address"),
|
||||
}),
|
||||
onSubmit: async (_) => {
|
||||
dispatch("save", $form.email);
|
||||
}),
|
||||
onSubmit: async (form) => {
|
||||
dispatch("save", form.email);
|
||||
closeModal!();
|
||||
},
|
||||
});
|
||||
@@ -41,13 +45,8 @@
|
||||
bind:openModal={_openModal}
|
||||
bind:closeModal
|
||||
>
|
||||
<form id="email-form" slot="content" on:submit={handleSubmit}>
|
||||
<TextField
|
||||
name="email"
|
||||
bind:value={$form.email}
|
||||
error={$errors.email}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
<form id="email-form" slot="content" use:form>
|
||||
<TextField name="email" error={$errors.email}></TextField>
|
||||
</form>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}
|
||||
|
||||
@@ -3,46 +3,61 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { object, string } from "yup";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { z } from "zod";
|
||||
|
||||
let _openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
export function openModal() {
|
||||
$form.password = ""
|
||||
$errors.password = "";
|
||||
$form.oldPassword = ""
|
||||
$errors.oldPassword = "";
|
||||
setFields("password", "")
|
||||
setFields("oldPassword", "")
|
||||
setErrors("password", [])
|
||||
setErrors("oldPassword", [])
|
||||
_openModal!();
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<{
|
||||
const { form, errors, setFields, setErrors } = createForm<{
|
||||
oldPassword: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
}>({
|
||||
initialValues: { oldPassword: "", password: "" },
|
||||
validationSchema: object({
|
||||
oldPassword: string()
|
||||
.min(
|
||||
extend: validator({
|
||||
schema: z
|
||||
.object({
|
||||
oldPassword: z.string().min(
|
||||
8,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 8 },
|
||||
}),
|
||||
)
|
||||
.required($_("required")),
|
||||
password: string()
|
||||
.min(
|
||||
),
|
||||
password: z.string().min(
|
||||
8,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 8 },
|
||||
}),
|
||||
)
|
||||
.required($_("required")),
|
||||
),
|
||||
passwordConfirm: z.string().min(
|
||||
8,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 8 },
|
||||
}),
|
||||
onSubmit: async (_) => {
|
||||
dispatch("save", {oldPassword: $form.oldPassword, password: $form.password, passwordConfirm: $form.password});
|
||||
),
|
||||
})
|
||||
.refine((d) => d.password === d.passwordConfirm, {
|
||||
message: "passwords-must-match",
|
||||
path: ["passwordConfirm"],
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (form) => {
|
||||
dispatch("save", {
|
||||
oldPassword: form.oldPassword,
|
||||
password: form.password,
|
||||
passwordConfirm: form.password,
|
||||
});
|
||||
closeModal!();
|
||||
},
|
||||
});
|
||||
@@ -55,30 +70,35 @@
|
||||
bind:openModal={_openModal}
|
||||
bind:closeModal
|
||||
>
|
||||
<form id="password-form" slot="content" on:submit={handleSubmit}>
|
||||
<form id="password-form" slot="content" use:form>
|
||||
<TextField
|
||||
name="oldPassword"
|
||||
label={$_("current-password")}
|
||||
type="password"
|
||||
bind:value={$form.oldPassword}
|
||||
error={$errors.oldPassword}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
label={$_("new-password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
error={$errors.password}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="passwordConfirm"
|
||||
label={$_("password-confirm")}
|
||||
type="password"
|
||||
error={$errors.passwordConfirm}
|
||||
></TextField>
|
||||
</form>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<button class="btn-primary" type="submit" form="password-form" name="save"
|
||||
>{$_("save")}</button
|
||||
<button
|
||||
class="btn-primary"
|
||||
type="submit"
|
||||
form="password-form"
|
||||
name="save">{$_("save")}</button
|
||||
>
|
||||
</div></Modal
|
||||
>
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { type SummitLog } from "$lib/models/summit_log";
|
||||
import {
|
||||
SummitLogCreateSchema,
|
||||
} from "$lib/models/api/summit_log_schema";
|
||||
import GPX from "$lib/models/gpx/gpx";
|
||||
import { summitLog } from "$lib/stores/summit_log_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||
import { createForm } from "felte";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { date, object, string } from "yup";
|
||||
import { z } from "zod";
|
||||
import Datepicker from "../base/datepicker.svelte";
|
||||
import Modal from "../base/modal.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import TrailPicker from "../trail/trail_picker.svelte";
|
||||
import GPX from "$lib/models/gpx/gpx";
|
||||
import PhotoPicker from "../trail/photo_picker.svelte";
|
||||
import TrailPicker from "../trail/trail_picker.svelte";
|
||||
import { cloneDeep } from "$lib/util/deep_util";
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const summitLogSchema = object<SummitLog>({
|
||||
id: string().optional(),
|
||||
date: date().required("Required").typeError($_("invalid-date")),
|
||||
text: string().optional(),
|
||||
const ClientSummitLogCreateSchema = SummitLogCreateSchema.extend({
|
||||
_photos: z.array(z.instanceof(File)).optional(),
|
||||
_gpx: z.instanceof(File).optional().nullable(),
|
||||
expand: z
|
||||
.object({
|
||||
gpx_data: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<SummitLog>({
|
||||
const { form, errors, data, setFields } = createForm<
|
||||
z.infer<typeof ClientSummitLogCreateSchema>
|
||||
>({
|
||||
initialValues: $summitLog,
|
||||
validationSchema: summitLogSchema,
|
||||
onSubmit: async (submittedValues) => {
|
||||
if (!$form.expand.gpx_data) {
|
||||
$form.gpx = "";
|
||||
extend: validator({ schema: ClientSummitLogCreateSchema }),
|
||||
onSubmit: async (form) => {
|
||||
if (!form.expand?.gpx_data) {
|
||||
form.gpx = "";
|
||||
}
|
||||
|
||||
if (
|
||||
!$form._photos?.length &&
|
||||
!$form.photos?.length &&
|
||||
$form.expand?.gpx_data
|
||||
!form._photos?.length &&
|
||||
!form.photos?.length &&
|
||||
form.expand?.gpx_data
|
||||
) {
|
||||
const canvas = document.querySelector(
|
||||
"#trail-picker-map .maplibregl-canvas",
|
||||
@@ -44,25 +53,25 @@
|
||||
const dataURL = canvas.toDataURL();
|
||||
const response = await fetch(dataURL);
|
||||
const blob = await response.blob();
|
||||
$form._photos = [new File([blob], "route")];
|
||||
form._photos = [new File([blob], "route")];
|
||||
}
|
||||
|
||||
dispatch("save", submittedValues);
|
||||
dispatch("save", form);
|
||||
closeModal!();
|
||||
},
|
||||
});
|
||||
$: form.set(util.cloneDeep($summitLog));
|
||||
$: setFields(cloneDeep($summitLog));
|
||||
|
||||
$: if ($summitLog._gpx) {
|
||||
$form._gpx = $summitLog._gpx;
|
||||
$data._gpx = $summitLog._gpx;
|
||||
}
|
||||
|
||||
async function handleTrailSelection(trailData: string | null) {
|
||||
if (!trailData) {
|
||||
$form.duration = undefined;
|
||||
$form.elevation_gain = undefined;
|
||||
$form.elevation_loss = undefined;
|
||||
$form.distance = undefined;
|
||||
$data.duration = undefined;
|
||||
$data.elevation_gain = undefined;
|
||||
$data.elevation_loss = undefined;
|
||||
$data.distance = undefined;
|
||||
return;
|
||||
}
|
||||
const gpxObject = await GPX.parse(trailData);
|
||||
@@ -72,16 +81,15 @@
|
||||
|
||||
const totals = gpxObject.getTotals();
|
||||
|
||||
$form.duration = totals.duration / 1000;
|
||||
$form.elevation_gain = totals.elevationGain;
|
||||
$form.elevation_loss = totals.elevationLoss;
|
||||
$form.distance = totals.distance;
|
||||
$data.duration = totals.duration / 1000;
|
||||
$data.elevation_gain = totals.elevationGain;
|
||||
$data.elevation_loss = totals.elevationLoss;
|
||||
$data.distance = totals.distance;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
id="summit-log-modal"
|
||||
title={$form.id ? $_("edit-entry") : $_("add-entry")}
|
||||
title={$data.id ? $_("edit-entry") : $_("add-entry")}
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
@@ -91,15 +99,10 @@
|
||||
id="summit-log-form"
|
||||
slot="content"
|
||||
class="modal-content space-y-4"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
<div class="flex">
|
||||
<Datepicker
|
||||
name="date"
|
||||
label={$_("date")}
|
||||
bind:value={$form.date}
|
||||
error={$errors.date}
|
||||
on:change={handleChange}
|
||||
<Datepicker name="date" label={$_("date")} error={$errors.date}
|
||||
></Datepicker>
|
||||
</div>
|
||||
<div>
|
||||
@@ -108,27 +111,27 @@
|
||||
</label>
|
||||
<PhotoPicker
|
||||
id="summitlog-photo-input"
|
||||
parent={$form}
|
||||
bind:photos={$form.photos}
|
||||
bind:photoFiles={$form._photos}
|
||||
parent={$data}
|
||||
bind:photos={$data.photos}
|
||||
bind:photoFiles={$data._photos}
|
||||
showThumbnailControls={false}
|
||||
></PhotoPicker>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
{#if $data.expand}
|
||||
<TrailPicker
|
||||
bind:trailFile={$form._gpx}
|
||||
bind:trailData={$form.expand.gpx_data}
|
||||
bind:trailFile={$data._gpx}
|
||||
bind:trailData={$data.expand.gpx_data}
|
||||
label={$_("trail", { values: { n: 1 } })}
|
||||
on:change={(e) => handleTrailSelection(e.detail)}
|
||||
></TrailPicker>
|
||||
{/if}
|
||||
<div class="basis-full">
|
||||
<Textarea
|
||||
name="text"
|
||||
extraClasses="h-28"
|
||||
label={$_("text")}
|
||||
bind:value={$form.text}
|
||||
error={$errors.text}
|
||||
on:change={handleChange}
|
||||
></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { fromFile, toGeoJson } from "$lib/util/gpx_util";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
export let trailFile: File | null;
|
||||
export let trailFile: File | undefined | null;
|
||||
export let trailData: string | undefined;
|
||||
export let label: string = "";
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
attributionControl: false,
|
||||
dragPan: false,
|
||||
scrollZoom: false,
|
||||
preserveDrawingBuffer: true
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,8 +96,11 @@
|
||||
});
|
||||
layer = map.getLayer(layerId) as M.LineLayerSpecification;
|
||||
source = map.getSource(sourceId) as M.GeoJSONSource;
|
||||
map.resize()
|
||||
map.fitBounds(geojson.bbox as M.LngLatBoundsLike, {animate: false, padding: 8});
|
||||
map.resize();
|
||||
map.fitBounds(geojson.bbox as M.LngLatBoundsLike, {
|
||||
animate: false,
|
||||
padding: 8,
|
||||
});
|
||||
}
|
||||
|
||||
function removeTrailFromMap() {
|
||||
|
||||
@@ -1,42 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { WaypointCreateSchema } from "$lib/models/api/waypoint_schema";
|
||||
import { convertDMSToDD } from "$lib/models/gpx/utils";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { cloneDeep } from "$lib/util/deep_util";
|
||||
import { icons } from "$lib/util/icon_util";
|
||||
import EXIF from "$lib/vendor/exif-js/exif";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { z } from "zod";
|
||||
import Combobox from "../base/combobox.svelte";
|
||||
import Modal from "../base/modal.svelte";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import PhotoPicker from "../trail/photo_picker.svelte";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { convertDMSToDD } from "$lib/models/gpx/utils";
|
||||
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<Waypoint>({
|
||||
const ClientWaypointCreateSchema = WaypointCreateSchema.extend({
|
||||
_photos: z.array(z.instanceof(File)).optional(),
|
||||
});
|
||||
|
||||
const { form, errors, data, setFields } = createForm<
|
||||
z.infer<typeof ClientWaypointCreateSchema>
|
||||
>({
|
||||
initialValues: $waypoint,
|
||||
validationSchema: waypointSchema,
|
||||
onSubmit: async (submittedWaypoint) => {
|
||||
dispatch("save", submittedWaypoint);
|
||||
extend: validator({ schema: ClientWaypointCreateSchema }),
|
||||
onSubmit: async (form) => {
|
||||
dispatch("save", form);
|
||||
|
||||
closeModal!();
|
||||
},
|
||||
transform: (values: unknown) => {
|
||||
const v = values as any;
|
||||
return {
|
||||
...v,
|
||||
lat: parseFloat(v.lat),
|
||||
lon: parseFloat(v.lon),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
$: form.set(util.cloneDeep($waypoint));
|
||||
$: setFields(cloneDeep($waypoint));
|
||||
|
||||
$: filteredIcons =
|
||||
($form.icon?.length ?? 0) > 2
|
||||
($data.icon?.length ?? 0) > 2
|
||||
? icons
|
||||
.filter((i) => i.includes($form.icon ?? ""))
|
||||
.filter((i) => i.includes($data.icon ?? ""))
|
||||
.map((i) => ({ text: i, value: i, icon: i }))
|
||||
: [];
|
||||
|
||||
@@ -48,8 +64,8 @@
|
||||
const lonDir = EXIF.getTag(p, "GPSLongitudeRef");
|
||||
|
||||
if (lat && lon) {
|
||||
$form.lat = convertDMSToDD(lat, latDir);
|
||||
$form.lon = convertDMSToDD(lon, lonDir);
|
||||
setFields("lat", convertDMSToDD(lat, latDir));
|
||||
setFields("lon", convertDMSToDD(lon, lonDir));
|
||||
} else {
|
||||
show_toast({
|
||||
text: "No GPS data in image",
|
||||
@@ -63,7 +79,7 @@
|
||||
|
||||
<Modal
|
||||
id="waypoint-modal"
|
||||
title={$form.id ? $_("edit-waypoint") : $_("add-waypoint")}
|
||||
title={$data.id ? $_("edit-waypoint") : $_("add-waypoint")}
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
@@ -73,50 +89,31 @@
|
||||
id="waypoint-form"
|
||||
slot="content"
|
||||
class="modal-content space-y-4"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="basis-2/3">
|
||||
<TextField
|
||||
name="name"
|
||||
label={$_("name")}
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
on:change={handleChange}
|
||||
<TextField name="name" label={$_("name")} error={$errors.name}
|
||||
></TextField>
|
||||
</div>
|
||||
|
||||
<Combobox
|
||||
name="icon"
|
||||
bind:value={$form.icon}
|
||||
icon={$form.icon}
|
||||
icon={$data.icon}
|
||||
items={filteredIcons}
|
||||
label={$_("icon")}
|
||||
on:change={handleChange}
|
||||
></Combobox>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
label={$_("description")}
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
on:change={handleChange}
|
||||
></Textarea>
|
||||
<div class="flex gap-4">
|
||||
<TextField
|
||||
name="lat"
|
||||
label={$_("latitude")}
|
||||
bind:value={$form.lat}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
<TextField name="lat" label={$_("latitude")} error={$errors.lat}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="lon"
|
||||
label={$_("longitude")}
|
||||
bind:value={$form.lon}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
<TextField name="lon" label={$_("longitude")} error={$errors.lat}
|
||||
></TextField>
|
||||
</div>
|
||||
<div>
|
||||
@@ -125,10 +122,10 @@
|
||||
</label>
|
||||
<PhotoPicker
|
||||
id="waypoint-photo-input"
|
||||
parent={$form}
|
||||
parent={$data}
|
||||
on:exif={(e) => getCoordinatesFromPhoto(e.detail)}
|
||||
bind:photos={$form.photos}
|
||||
bind:photoFiles={$form._photos}
|
||||
bind:photos={$data.photos}
|
||||
bind:photoFiles={$data._photos}
|
||||
showThumbnailControls={false}
|
||||
showExifControls={true}
|
||||
></PhotoPicker>
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Höhenunterschied (aufw.)",
|
||||
"elevation-loss": "Höhenunterschied (abw.)",
|
||||
"email": "Email",
|
||||
"email-updated": "",
|
||||
"empty-activities": "{username} hat noch keine Aktivitäten",
|
||||
"empty-bio": "{username} hat noch keine Biographie hinzugefügt",
|
||||
"empty-lists": "{username} hat keine öffentlichen Listen",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "Kein Gitter",
|
||||
"no-preference": "Keine Präferenz",
|
||||
"no-results": "Keine Ergebnisse gefunden",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Keine gültige Email-Adresse",
|
||||
"not-completed": "Nicht abgeschlossen",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Elevation Gain",
|
||||
"elevation-loss": "Elevation Loss",
|
||||
"email": "Email",
|
||||
"email-updated": "Email updated",
|
||||
"empty-activities": "{username} has no activity yet",
|
||||
"empty-bio": "{username} has not added a bio yet",
|
||||
"empty-lists": "{username} has no public lists",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "No Grid",
|
||||
"no-preference": "No preference",
|
||||
"no-results": "No results found",
|
||||
"no-routes-added": "No routes added",
|
||||
"not-a-valid-email-address": "Not a valid email address",
|
||||
"not-completed": "Not completed",
|
||||
"notification-list-create": "{user} created a new list",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Gain d'altitude",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "Pas de préférence",
|
||||
"no-results": "Pas de résultat",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Adresse email invalide",
|
||||
"not-completed": "Pas terminé",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Magasságnövekedés",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "Nincs preferált",
|
||||
"no-results": "Nincs eredmény",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Érvénytelen e-mail cím",
|
||||
"not-completed": "Nem teljesített",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Guadagno di quota",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "Nessuna preferenza",
|
||||
"no-results": "Nessun risultato trovato",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Indirizzo email non valido",
|
||||
"not-completed": "Non completato",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Hoogteverschil",
|
||||
"elevation-loss": "",
|
||||
"email": "E-mail",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "Geen voorkeur",
|
||||
"no-results": "Er zijn geen zoekresultaten",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Het e-mailadres is ongeldig",
|
||||
"not-completed": "Niet voltooid",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Wzrost Wysokości",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "Brak preferencji",
|
||||
"no-results": "Brak wyników",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Nieprawidłowy adres email",
|
||||
"not-completed": "Nie dokończono",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "Ganho de elevação",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "Nenhuma preferência",
|
||||
"no-results": "Nenhum resultado encontrado",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "Não um endereço de e-mail válido",
|
||||
"not-completed": "Não preenchido",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"elevation-gain": "上升海拔",
|
||||
"elevation-loss": "",
|
||||
"email": "电子邮箱",
|
||||
"email-updated": "",
|
||||
"empty-activities": "",
|
||||
"empty-bio": "",
|
||||
"empty-lists": "",
|
||||
@@ -175,6 +176,7 @@
|
||||
"no-grid": "",
|
||||
"no-preference": "尚未规划",
|
||||
"no-results": "没有找到结果",
|
||||
"no-routes-added": "",
|
||||
"not-a-valid-email-address": "无效电子邮箱地址",
|
||||
"not-completed": "未完成",
|
||||
"notification-list-create": "",
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z, ZodType } from "zod";
|
||||
import type { List } from "../list";
|
||||
|
||||
const ListCreateSchema = z.object({
|
||||
name: z.string(),
|
||||
id: z.string().length(15).optional(),
|
||||
name: z.string().min(1, "required"),
|
||||
public: z.boolean(),
|
||||
description: z.string().optional(),
|
||||
trails: z.array(z.string().length(15)),
|
||||
avatar: z.string().optional(),
|
||||
author: z.string()
|
||||
}) satisfies ZodType<List>
|
||||
|
||||
@@ -17,7 +17,6 @@ const ListUpdateSchema = z.object({
|
||||
trails: z.array(z.string().length(15)).optional(),
|
||||
"trails-": z.string().optional(),
|
||||
"trails+": z.string().optional(),
|
||||
avatar: z.string().optional(),
|
||||
}) satisfies ZodType<Partial<List>>
|
||||
|
||||
export { ListCreateSchema, ListUpdateSchema };
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SummitLog } from "../summit_log";
|
||||
|
||||
const SummitLogCreateSchema = z.object({
|
||||
id: z.string().length(15).optional(),
|
||||
date: z.string().date(),
|
||||
date: z.string().date().refine((val) => !isNaN(Date.parse(val)), "invalid-date"),
|
||||
text: z.string().optional(),
|
||||
gpx: z.string().optional(),
|
||||
distance: z.number().nonnegative().optional(),
|
||||
@@ -16,7 +16,7 @@ const SummitLogCreateSchema = z.object({
|
||||
}) satisfies ZodType<SummitLog>
|
||||
|
||||
const SummitLogUpdateSchema = z.object({
|
||||
date: z.string().date().optional(),
|
||||
date: z.string().date().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date").optional(),
|
||||
text: z.string().optional(),
|
||||
gpx: z.string().optional(),
|
||||
distance: z.number().nonnegative().optional(),
|
||||
|
||||
@@ -3,10 +3,11 @@ import type { Trail } from "../trail";
|
||||
|
||||
|
||||
const TrailCreateSchema = z.object({
|
||||
name: z.string(),
|
||||
id: z.string().length(15).optional(),
|
||||
name: z.string().min(1, "required"),
|
||||
description: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
date: z.string().date(),
|
||||
date: z.string().optional().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date"),
|
||||
public: z.boolean(),
|
||||
difficulty: z.enum(["easy", "moderate", "difficult"]).optional(),
|
||||
lat: z.number().optional(),
|
||||
@@ -29,7 +30,7 @@ const TrailUpdateSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
date: z.string().date(),
|
||||
date: z.string().optional().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date"),
|
||||
public: z.boolean().optional(),
|
||||
difficulty: z.enum(["easy", "moderate", "difficult"]).optional(),
|
||||
lat: z.number().optional(),
|
||||
|
||||
@@ -17,8 +17,8 @@ const WaypointUpdateSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
lat: z.number().optional(),
|
||||
lon: z.number().optional(),
|
||||
lat: z.number({coerce: true}).optional(),
|
||||
lon: z.number({coerce: true}).optional(),
|
||||
icon: z.string().optional(),
|
||||
photos: z.array(z.string()),
|
||||
"photos-": z.string().optional(),
|
||||
|
||||
@@ -16,7 +16,7 @@ export class List {
|
||||
author?: UserAnonymous;
|
||||
|
||||
}
|
||||
author?: string;
|
||||
author: string;
|
||||
|
||||
constructor(name: string, trails: Trail[], params?: { description?: string, public?: boolean, avatar?: string, author?: string }) {
|
||||
this.name = name;
|
||||
@@ -24,8 +24,8 @@ export class List {
|
||||
this.expand = { trails: trails };
|
||||
this.trails = trails.map(t => t.id!);
|
||||
this.description = params?.description;
|
||||
this.avatar = params?.description;
|
||||
this.author = params?.author;
|
||||
this.avatar = params?.avatar;
|
||||
this.author = params?.author ?? "000000000000000";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class SummitLog {
|
||||
elevation_gain?: number
|
||||
elevation_loss?: number
|
||||
duration?: number
|
||||
author?: string;
|
||||
author: string;
|
||||
|
||||
expand?: {
|
||||
gpx_data?: string;
|
||||
@@ -33,6 +33,7 @@ class SummitLog {
|
||||
this._gpx = null
|
||||
this.photos = params?.photos ?? []
|
||||
this._photos = [];
|
||||
this.author = "000000000000000"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Trail {
|
||||
}
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
author?: string;
|
||||
author: string;
|
||||
|
||||
constructor(name: string,
|
||||
params?: {
|
||||
@@ -70,10 +70,10 @@ class Trail {
|
||||
this.location = params?.location;
|
||||
this.date = params?.date ?? new Date().toISOString().split('T')[0];
|
||||
this.public = params?.public ?? false
|
||||
this.distance = params?.distance;
|
||||
this.elevation_gain = params?.elevation_gain;
|
||||
this.elevation_loss = params?.elevation_loss;
|
||||
this.duration = params?.duration;
|
||||
this.distance = params?.distance ?? 0;
|
||||
this.elevation_gain = params?.elevation_gain ?? 0;
|
||||
this.elevation_loss = params?.elevation_loss ?? 0;
|
||||
this.duration = params?.duration ?? 0;
|
||||
this.difficulty = params?.difficulty ?? "easy";
|
||||
this.lat = params?.lat;
|
||||
this.lon = params?.lon;
|
||||
@@ -92,6 +92,7 @@ class Trail {
|
||||
this.tags = params?.tags ?? []
|
||||
this.description = params?.description ?? "";
|
||||
this.created = params?.created;
|
||||
this.author = "000000000000000"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Marker } from "maplibre-gl";
|
||||
import * as M from "maplibre-gl";
|
||||
import { number, object, string } from "yup";
|
||||
|
||||
@@ -12,7 +11,7 @@ class Waypoint {
|
||||
marker?: M.Marker;
|
||||
photos: string[];
|
||||
_photos?: File[];
|
||||
author?: string;
|
||||
author: string;
|
||||
|
||||
constructor(lat: number, lon: number, params?: {
|
||||
id?: string, name?: string, description?: string, icon?: string, marker?: M.Marker, photos?: string[];
|
||||
@@ -26,16 +25,8 @@ class Waypoint {
|
||||
this.marker = params?.marker;
|
||||
this.photos = params?.photos ?? []
|
||||
this._photos = [];
|
||||
this.author = "000000000000000"
|
||||
}
|
||||
}
|
||||
|
||||
const waypointSchema = object<Waypoint>({
|
||||
id: string().optional(),
|
||||
name: string().optional(),
|
||||
description: string().optional(),
|
||||
lat: number().required('Required').typeError('Invalid latitude'),
|
||||
lon: number().required('Required').typeError('Invalid longitude'),
|
||||
icon: string().optional()
|
||||
});
|
||||
|
||||
export { Waypoint, waypointSchema };
|
||||
export { Waypoint };
|
||||
|
||||
@@ -2,14 +2,14 @@ import type { SummitLog } from "$lib/models/summit_log";
|
||||
import { Trail, type TrailFilter, type TrailFilterValues } from "$lib/models/trail";
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { deepEqual } from "$lib/util/deep_util";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||
import * as M from "maplibre-gl";
|
||||
import type { Hits } from "meilisearch";
|
||||
import { ClientResponseError, type ListResult } from "pocketbase";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_store";
|
||||
import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store";
|
||||
import type { Hits, SearchResponse } from "meilisearch";
|
||||
|
||||
let trails: Trail[] = []
|
||||
export const trail: Writable<Trail> = writable(new Trail(""));
|
||||
@@ -469,7 +469,7 @@ function compareObjectArrays<T extends { id?: string }>(oldArray: T[], newArray:
|
||||
const oldObj = oldArray.find(oldObj => oldObj.id === newObj.id)
|
||||
if (!oldObj) {
|
||||
newObjects.push(newObj);
|
||||
} else if (!util.deepEqual(newObj, oldObj)) {
|
||||
} else if (!deepEqual(newObj, oldObj)) {
|
||||
updatedObjects.push(newObj);
|
||||
} else {
|
||||
unchangedObjects.push(newObj);
|
||||
|
||||
37
web/src/lib/util/deep_util.ts
Normal file
37
web/src/lib/util/deep_util.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
const has = Object.prototype.hasOwnProperty;
|
||||
|
||||
export function deepEqual(foo: any, bar: any) {
|
||||
var ctor, len;
|
||||
if (foo === bar) return true;
|
||||
|
||||
if (foo && bar && (ctor = foo.constructor) === bar.constructor) {
|
||||
if (ctor === Date) return foo.getTime() === bar.getTime();
|
||||
if (ctor === RegExp) return foo.toString() === bar.toString();
|
||||
|
||||
if (ctor === Array) {
|
||||
if ((len = foo.length) === bar.length) {
|
||||
while (len-- && deepEqual(foo[len], bar[len]));
|
||||
}
|
||||
return len === -1;
|
||||
}
|
||||
|
||||
if (!ctor || typeof foo === 'object') {
|
||||
len = 0;
|
||||
for (ctor in foo) {
|
||||
if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;
|
||||
if (!(ctor in bar) || !deepEqual(foo[ctor], bar[ctor])) return false;
|
||||
}
|
||||
return Object.keys(bar).length === len;
|
||||
}
|
||||
}
|
||||
|
||||
return foo !== foo && bar !== bar;
|
||||
}
|
||||
|
||||
export function cloneDeep(foo: any) {
|
||||
try {
|
||||
return structuredClone(foo);
|
||||
} catch (e) {
|
||||
return foo;
|
||||
}
|
||||
}
|
||||
261
web/src/lib/vendor/svelte-form-lib/create-form.js
vendored
261
web/src/lib/vendor/svelte-form-lib/create-form.js
vendored
@@ -1,261 +0,0 @@
|
||||
import {derived, writable, get} from 'svelte/store';
|
||||
import {util} from './util';
|
||||
|
||||
const NO_ERROR = '';
|
||||
const IS_TOUCHED = true;
|
||||
|
||||
function isCheckbox(element) {
|
||||
return element.getAttribute && element.getAttribute('type') === 'checkbox';
|
||||
}
|
||||
|
||||
function isFileInput(element) {
|
||||
return element.getAttribute && element.getAttribute('type') === 'file';
|
||||
}
|
||||
|
||||
function resolveValue(element) {
|
||||
if (isFileInput(element)) {
|
||||
return element.files;
|
||||
} else if (isCheckbox(element)) {
|
||||
return element.checked;
|
||||
} else {
|
||||
return element.value;
|
||||
}
|
||||
}
|
||||
|
||||
export const createForm = (config) => {
|
||||
let initialValues = config.initialValues || {};
|
||||
|
||||
const validationSchema = config.validationSchema;
|
||||
const validateFunction = config.validate;
|
||||
const onSubmit = config.onSubmit;
|
||||
const onError = config.onError;
|
||||
|
||||
const getInitial = {
|
||||
values: () => util.cloneDeep(initialValues),
|
||||
errors: () =>
|
||||
validationSchema
|
||||
? util.getErrorsFromSchema(initialValues, validationSchema.fields)
|
||||
: util.assignDeep(initialValues, NO_ERROR),
|
||||
touched: () => util.assignDeep(initialValues, !IS_TOUCHED),
|
||||
};
|
||||
|
||||
const form = writable(getInitial.values());
|
||||
const errors = writable(getInitial.errors());
|
||||
const touched = writable(getInitial.touched());
|
||||
|
||||
const isSubmitting = writable(false);
|
||||
const isValidating = writable(false);
|
||||
|
||||
const isValid = derived(errors, ($errors) => {
|
||||
const noErrors = util
|
||||
.getValues($errors)
|
||||
.every((field) => field === NO_ERROR);
|
||||
return noErrors;
|
||||
});
|
||||
|
||||
const modified = derived(form, ($form) => {
|
||||
const object = util.assignDeep($form, false);
|
||||
|
||||
for (let key in $form) {
|
||||
object[key] = !util.deepEqual($form[key], initialValues[key]);
|
||||
}
|
||||
|
||||
return object;
|
||||
});
|
||||
|
||||
const isModified = derived(modified, ($modified) => {
|
||||
return util.getValues($modified).includes(true);
|
||||
});
|
||||
|
||||
function validateField(field) {
|
||||
return util
|
||||
.subscribeOnce(form)
|
||||
.then((values) => validateFieldValue(field, values[field]));
|
||||
}
|
||||
|
||||
function validateFieldValue(field, value) {
|
||||
updateTouched(field, true);
|
||||
|
||||
if (validationSchema) {
|
||||
isValidating.set(true);
|
||||
|
||||
return validationSchema
|
||||
.validateAt(field, get(form))
|
||||
.then(() => util.update(errors, field, ''))
|
||||
.catch((error) => util.update(errors, field, error.message))
|
||||
.finally(() => {
|
||||
isValidating.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (validateFunction) {
|
||||
isValidating.set(true);
|
||||
return Promise.resolve()
|
||||
.then(() => validateFunction({[field]: value}))
|
||||
.then((errs) =>
|
||||
util.update(errors, field, !util.isNullish(errs) ? errs[field] : ''),
|
||||
)
|
||||
.finally(() => {
|
||||
isValidating.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function updateValidateField(field, value) {
|
||||
updateField(field, value);
|
||||
return validateFieldValue(field, value);
|
||||
}
|
||||
|
||||
function handleChange(event) {
|
||||
const element = event.target;
|
||||
const field = element.name || element.id;
|
||||
const value = resolveValue(element);
|
||||
|
||||
return updateValidateField(field, value);
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
isSubmitting.set(true);
|
||||
|
||||
return util.subscribeOnce(form).then((values) => {
|
||||
if (typeof validateFunction === 'function') {
|
||||
isValidating.set(true);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => validateFunction(values))
|
||||
.then((error) => {
|
||||
if (util.isNullish(error) || util.getValues(error).length === 0) {
|
||||
return clearErrorsAndSubmit(values);
|
||||
} else {
|
||||
errors.set(error);
|
||||
if(onError) {
|
||||
onError(error)
|
||||
}
|
||||
isSubmitting.set(false);
|
||||
}
|
||||
})
|
||||
.finally(() => isValidating.set(false));
|
||||
}
|
||||
|
||||
if (validationSchema) {
|
||||
isValidating.set(true);
|
||||
|
||||
return (
|
||||
validationSchema
|
||||
.validate(values, {abortEarly: false})
|
||||
.then(() => clearErrorsAndSubmit(values))
|
||||
// eslint-disable-next-line unicorn/catch-error-name
|
||||
.catch((yupErrors) => {
|
||||
if (yupErrors && yupErrors.inner) {
|
||||
const updatedErrors = getInitial.errors();
|
||||
|
||||
yupErrors.inner.map((error) =>
|
||||
util.set(updatedErrors, error.path, error.message),
|
||||
);
|
||||
if(onError) {
|
||||
onError(updatedErrors)
|
||||
}
|
||||
errors.set(updatedErrors);
|
||||
}
|
||||
isSubmitting.set(false);
|
||||
})
|
||||
.finally(() => isValidating.set(false))
|
||||
);
|
||||
}
|
||||
|
||||
return clearErrorsAndSubmit(values);
|
||||
});
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
form.set(getInitial.values());
|
||||
errors.set(getInitial.errors());
|
||||
touched.set(getInitial.touched());
|
||||
}
|
||||
|
||||
function clearErrorsAndSubmit(values) {
|
||||
return Promise.resolve()
|
||||
.then(() => errors.set(getInitial.errors()))
|
||||
.then(() => onSubmit(values, form, errors))
|
||||
.finally(() => isSubmitting.set(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler to imperatively update the value of a form field
|
||||
*/
|
||||
function updateField(field, value) {
|
||||
util.update(form, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler to imperatively update the touched value of a form field
|
||||
*/
|
||||
function updateTouched(field, value) {
|
||||
util.update(touched, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the initial values and reset form. Used to dynamically display new form values
|
||||
*/
|
||||
function updateInitialValues(newValues) {
|
||||
initialValues = newValues;
|
||||
|
||||
handleReset();
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
errors,
|
||||
touched,
|
||||
modified,
|
||||
isValid,
|
||||
isSubmitting,
|
||||
isValidating,
|
||||
isModified,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
handleReset,
|
||||
updateField,
|
||||
updateValidateField,
|
||||
updateTouched,
|
||||
validateField,
|
||||
updateInitialValues,
|
||||
state: derived(
|
||||
[
|
||||
form,
|
||||
errors,
|
||||
touched,
|
||||
modified,
|
||||
isValid,
|
||||
isValidating,
|
||||
isSubmitting,
|
||||
isModified,
|
||||
],
|
||||
([
|
||||
$form,
|
||||
$errors,
|
||||
$touched,
|
||||
$modified,
|
||||
$isValid,
|
||||
$isValidating,
|
||||
$isSubmitting,
|
||||
$isModified,
|
||||
]) => ({
|
||||
form: $form,
|
||||
errors: $errors,
|
||||
touched: $touched,
|
||||
modified: $modified,
|
||||
isValid: $isValid,
|
||||
isSubmitting: $isSubmitting,
|
||||
isValidating: $isValidating,
|
||||
isModified: $isModified,
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
103
web/src/lib/vendor/svelte-form-lib/index.d.ts
vendored
103
web/src/lib/vendor/svelte-form-lib/index.d.ts
vendored
@@ -1,103 +0,0 @@
|
||||
/// <reference lib="svelte2tsx" />
|
||||
import type {SvelteComponentTyped} from 'svelte';
|
||||
import type {Readable, Writable} from 'svelte/store';
|
||||
import type {ObjectSchema} from 'yup';
|
||||
|
||||
export type FormProps<Inf = Record<string, unknown>> = {
|
||||
context?: FormState;
|
||||
initialValues?: Inf;
|
||||
onSubmit?: ((values: Inf) => any) | ((values: Inf) => Promise<any>);
|
||||
validate?: (values: Inf) => any | undefined;
|
||||
validationSchema?: ObjectSchema<any>;
|
||||
} & svelte.JSX.HTMLAttributes<HTMLFormElement>;
|
||||
|
||||
type FieldProperties = {
|
||||
name: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLInputElement>;
|
||||
|
||||
type SelectProperties = {
|
||||
name: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLSelectElement>;
|
||||
|
||||
type ErrorProperties = {
|
||||
name: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLDivElement>;
|
||||
|
||||
type TextareaProperties = {
|
||||
name: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLTextAreaElement>;
|
||||
|
||||
type FormState<Inf = Record<string, any>> = {
|
||||
form: Writable<Inf>;
|
||||
errors: Writable<Record<keyof Inf, string>>;
|
||||
touched: Writable<Record<keyof Inf, boolean>>;
|
||||
modified: Readable<Record<keyof Inf, boolean>>;
|
||||
isValid: Readable<boolean>;
|
||||
isSubmitting: Writable<boolean>;
|
||||
isValidating: Writable<boolean>;
|
||||
isModified: Readable<boolean>;
|
||||
updateField: (field: keyof Inf, value: any) => void;
|
||||
updateValidateField: (field: keyof Inf, value: any) => void;
|
||||
updateTouched: (field: keyof Inf, value: any) => void;
|
||||
validateField: (field: keyof Inf) => Promise<any>;
|
||||
updateInitialValues: (newValues: Inf) => void;
|
||||
handleReset: () => void;
|
||||
state: Readable<{
|
||||
form: Inf;
|
||||
errors: Record<keyof Inf, string>;
|
||||
touched: Record<keyof Inf, boolean>;
|
||||
modified: Record<keyof Inf, boolean>;
|
||||
isValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
isValidating: boolean;
|
||||
isModified: boolean;
|
||||
}>;
|
||||
handleChange: (event: Event) => any;
|
||||
handleSubmit: (event: Event) => any;
|
||||
};
|
||||
|
||||
declare function createForm<Inf = Record<string, any>>(formProperties: {
|
||||
initialValues: Inf;
|
||||
onSubmit: (values: Inf) => any | Promise<any>;
|
||||
onError?: (errors: Record<string, any>) => any | Promise<any>;
|
||||
validate?: (values: Inf) => any | undefined;
|
||||
validationSchema?: ObjectSchema<any>;
|
||||
}): FormState<Inf>;
|
||||
|
||||
declare class Form extends SvelteComponentTyped<
|
||||
FormProps,
|
||||
Record<string, unknown>,
|
||||
{
|
||||
default: FormState;
|
||||
}
|
||||
> {}
|
||||
|
||||
declare class Field extends SvelteComponentTyped<
|
||||
FieldProperties,
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>
|
||||
> {}
|
||||
|
||||
declare class Textarea extends SvelteComponentTyped<
|
||||
TextareaProperties,
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>
|
||||
> {}
|
||||
|
||||
declare class Select extends SvelteComponentTyped<
|
||||
SelectProperties,
|
||||
Record<string, unknown>,
|
||||
{default: any}
|
||||
> {}
|
||||
|
||||
declare class ErrorMessage extends SvelteComponentTyped<
|
||||
ErrorProperties,
|
||||
Record<string, unknown>,
|
||||
{default: any}
|
||||
> {}
|
||||
|
||||
declare const key: {};
|
||||
|
||||
export {createForm, key, Form, Field, Select, ErrorMessage, Textarea};
|
||||
1
web/src/lib/vendor/svelte-form-lib/index.js
vendored
1
web/src/lib/vendor/svelte-form-lib/index.js
vendored
@@ -1 +0,0 @@
|
||||
export {createForm} from './create-form';
|
||||
134
web/src/lib/vendor/svelte-form-lib/util.js
vendored
134
web/src/lib/vendor/svelte-form-lib/util.js
vendored
@@ -1,134 +0,0 @@
|
||||
import { dequal as isEqual } from 'dequal/lite';
|
||||
|
||||
function subscribeOnce(observable) {
|
||||
return new Promise((resolve) => {
|
||||
observable.subscribe(resolve)(); // immediately invoke to unsubscribe
|
||||
});
|
||||
}
|
||||
|
||||
function update(object, path, value) {
|
||||
object.update((o) => {
|
||||
set(o, path, value);
|
||||
return o;
|
||||
});
|
||||
}
|
||||
|
||||
function cloneDeep(object) {
|
||||
try {
|
||||
return structuredClone(object);
|
||||
} catch (e) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
function isNullish(value) {
|
||||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
function isEmpty(object) {
|
||||
return isNullish(object) || Object.keys(object).length <= 0;
|
||||
}
|
||||
|
||||
function getValues(object) {
|
||||
let results = [];
|
||||
|
||||
for (const [, value] of Object.entries(object)) {
|
||||
const values = typeof value === 'object' ? getValues(value) : [value];
|
||||
results = [...results, ...values];
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// TODO: refactor this so as not to rely directly on yup's API
|
||||
// This should use dependency injection, with a default callback which may assume
|
||||
// yup as the validation schema
|
||||
function getErrorsFromSchema(initialValues, schema, errors = {}) {
|
||||
for (const key in schema) {
|
||||
switch (true) {
|
||||
case schema[key].type === 'object' && !isEmpty(schema[key].fields): {
|
||||
errors[key] = getErrorsFromSchema(
|
||||
initialValues[key],
|
||||
schema[key].fields,
|
||||
{ ...errors[key] },
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case schema[key].type === 'array': {
|
||||
const values =
|
||||
initialValues && initialValues[key] ? initialValues[key] : [];
|
||||
errors[key] = values.map((value) => {
|
||||
const innerError = getErrorsFromSchema(
|
||||
value,
|
||||
schema[key].innerType.fields,
|
||||
{ ...errors[key] },
|
||||
);
|
||||
|
||||
return Object.keys(innerError).length > 0 ? innerError : '';
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
errors[key] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const deepEqual = isEqual;
|
||||
|
||||
function assignDeep(object, value) {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map((o) => assignDeep(o, value));
|
||||
}
|
||||
const copy = {};
|
||||
for (const key in object) {
|
||||
copy[key] =
|
||||
typeof object[key] === 'object' && !isNullish(object[key]) ? assignDeep(object[key], value) : value;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function set(object, path, value) {
|
||||
if (new Object(object) !== object) return object;
|
||||
|
||||
if (!Array.isArray(path)) {
|
||||
path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
}
|
||||
|
||||
const result = path
|
||||
.slice(0, -1)
|
||||
// TODO: replace this reduce with something more readable
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
.reduce(
|
||||
(accumulator, key, index) =>
|
||||
new Object(accumulator[key]) === accumulator[key]
|
||||
? accumulator[key]
|
||||
: (accumulator[key] =
|
||||
Math.trunc(Math.abs(path[index + 1])) === +path[index + 1]
|
||||
? []
|
||||
: {}),
|
||||
object,
|
||||
);
|
||||
|
||||
result[path[path.length - 1]] = value;
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
export const util = {
|
||||
assignDeep,
|
||||
cloneDeep,
|
||||
deepEqual,
|
||||
getErrorsFromSchema,
|
||||
getValues,
|
||||
isEmpty,
|
||||
isNullish,
|
||||
set,
|
||||
subscribeOnce,
|
||||
update,
|
||||
};
|
||||
@@ -7,9 +7,11 @@ import { json, type RequestEvent } from "@sveltejs/kit";
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const r = await show<Comment>(event, Collection.comments)
|
||||
r.expand = {
|
||||
author: await pb.collection('users_anonymous').getOne(r.author)
|
||||
if (!r.expand) {
|
||||
r.expand = {} as any
|
||||
}
|
||||
r.expand!.author = await pb.collection('users_anonymous').getOne(r.author)
|
||||
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw handleError(e)
|
||||
|
||||
@@ -7,9 +7,11 @@ import { json, type RequestEvent } from "@sveltejs/kit";
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const r = await show<List>(event, Collection.lists)
|
||||
r.expand = {
|
||||
author: await pb.collection('users_anonymous').getOne(r.author!)
|
||||
if (!r.expand) {
|
||||
r.expand = {} as any
|
||||
}
|
||||
r.expand!.author = await pb.collection('users_anonymous').getOne(r.author!)
|
||||
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw handleError(e)
|
||||
|
||||
@@ -5,29 +5,41 @@
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
|
||||
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
|
||||
import type { User } from "$lib/models/user";
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import {
|
||||
users_confirm_reset,
|
||||
users_reset_password,
|
||||
} from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { users_confirm_reset } from "$lib/stores/user_store";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { object, ref, string } from "yup";
|
||||
import { z } from "zod";
|
||||
|
||||
let loading: boolean = false;
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm({
|
||||
const { form, errors } = createForm({
|
||||
initialValues: {
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
},
|
||||
validationSchema: object({
|
||||
password: string().required($_("required")),
|
||||
passwordConfirm: string()
|
||||
.required($_("required"))
|
||||
.oneOf([ref("password"), ""], $_('passwords-must-match')),
|
||||
extend: validator({
|
||||
schema: z
|
||||
.object({
|
||||
password: z.string().min(
|
||||
8,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 8 },
|
||||
}),
|
||||
),
|
||||
passwordConfirm: z.string().min(
|
||||
8,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 8 },
|
||||
}),
|
||||
),
|
||||
})
|
||||
.refine((d) => d.password === d.passwordConfirm, {
|
||||
message: "passwords-must-match",
|
||||
path: ["passwordConfirm"],
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (form) => {
|
||||
loading = true;
|
||||
@@ -61,7 +73,7 @@
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-4 w-[28rem] mt-8"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
{#if $theme == "light"}
|
||||
<LogoTextTwoLineDark></LogoTextTwoLineDark>
|
||||
@@ -74,16 +86,12 @@
|
||||
name="password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
error={$errors.password}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
name="passwordConfirm"
|
||||
label={$_("password-confirm")}
|
||||
type="password"
|
||||
bind:value={$form.passwordConfirm}
|
||||
on:change={handleChange}
|
||||
error={$errors.passwordConfirm}
|
||||
></TextField>
|
||||
|
||||
|
||||
@@ -4,22 +4,27 @@
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import LogoTextTwoLineDark from "$lib/components/logo/logo_text_two_line_dark.svelte";
|
||||
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
|
||||
import type { User } from "$lib/models/user";
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { users_reset_password } from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { object, string } from "yup";
|
||||
import { z } from "zod";
|
||||
|
||||
let loading: boolean = false;
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm({
|
||||
const { form, errors } = createForm({
|
||||
initialValues: {
|
||||
email: "",
|
||||
},
|
||||
validationSchema: object<User>({
|
||||
email: string().required($_("required")),
|
||||
extend: validator({
|
||||
schema: z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "required")
|
||||
.email("not-a-valid-email-address"),
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (email) => {
|
||||
loading = true;
|
||||
@@ -50,7 +55,7 @@
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-4 w-[28rem] mt-8"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
{#if $theme == "light"}
|
||||
<LogoTextTwoLineDark></LogoTextTwoLineDark>
|
||||
@@ -63,8 +68,6 @@
|
||||
<TextField
|
||||
name="email"
|
||||
label={$_("email")}
|
||||
bind:value={$form.email}
|
||||
on:change={handleChange}
|
||||
error={$errors.email}
|
||||
></TextField>
|
||||
<div class="grid grid-cols-2 gap-x-4">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { listSchema, type List } from "$lib/models/list";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||
import { type List } from "$lib/models/list";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
import { page } from "$app/stores";
|
||||
@@ -10,8 +9,12 @@
|
||||
} from "$lib/components/base/search.svelte";
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import Textarea from "$lib/components/base/textarea.svelte";
|
||||
import Toggle from "$lib/components/base/toggle.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||
import { ListCreateSchema } from "$lib/models/api/list_schema.js";
|
||||
import { ListShareCreateSchema } from "$lib/models/api/list_share_schema.js";
|
||||
import { TrailCreateSchema } from "$lib/models/api/trail_schema.js";
|
||||
import type { Trail } from "$lib/models/trail.js";
|
||||
import { TrailShare } from "$lib/models/trail_share.js";
|
||||
import { lists_create, lists_update } from "$lib/stores/list_store.js";
|
||||
@@ -21,14 +24,16 @@
|
||||
trail_share_index,
|
||||
} from "$lib/stores/trail_share_store.js";
|
||||
import { trails_show, trails_update } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store.js";
|
||||
import { getFileURL } from "$lib/util/file_util.js";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import Toggle from "$lib/components/base/toggle.svelte";
|
||||
import { currentUser } from "$lib/stores/user_store.js";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { z } from "zod";
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -46,13 +51,32 @@
|
||||
let openConfirmModal: () => void;
|
||||
let openPublishConfirmModal: () => void;
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<List>({
|
||||
const ClientListCreateSchema = ListCreateSchema.extend({
|
||||
_photos: z.array(z.instanceof(File)).optional(),
|
||||
avatar: z.string().or(z.instanceof(File)).optional(),
|
||||
expand: z
|
||||
.object({
|
||||
trails: z.array(TrailCreateSchema).optional(),
|
||||
list_share_via_list: z.array(ListShareCreateSchema).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
data: formData,
|
||||
} = createForm<z.infer<typeof ClientListCreateSchema>>({
|
||||
initialValues: {
|
||||
...data.list!,
|
||||
public: $page.data.settings?.privacy.lists === "public",
|
||||
...data.list,
|
||||
public: data.list.id
|
||||
? data.list.public
|
||||
: $page.data.settings?.privacy.lists === "public",
|
||||
},
|
||||
validationSchema: listSchema,
|
||||
onSubmit: async (submittedList) => {
|
||||
extend: validator({
|
||||
schema: ClientListCreateSchema,
|
||||
}),
|
||||
onSubmit: async (form) => {
|
||||
if (await checkPrerequisites()) {
|
||||
await saveList();
|
||||
}
|
||||
@@ -61,10 +85,10 @@
|
||||
|
||||
async function checkPrerequisites() {
|
||||
if (
|
||||
(data.list?.public === false && $form.public === true) ||
|
||||
($form.public === true &&
|
||||
(data.list?.public === false && $formData.public === true) ||
|
||||
($formData.public === true &&
|
||||
data.list?.expand?.trails?.length !==
|
||||
$form.expand?.trails?.length)
|
||||
$formData.expand?.trails?.length)
|
||||
) {
|
||||
openPublishConfirmModal();
|
||||
return false;
|
||||
@@ -82,11 +106,14 @@
|
||||
).files![0];
|
||||
loading = true;
|
||||
try {
|
||||
if ($form.id) {
|
||||
await lists_update($form, avatarFile);
|
||||
if ($formData.id) {
|
||||
await lists_update($formData as List, avatarFile);
|
||||
} else {
|
||||
const createdList = await lists_create($form, avatarFile);
|
||||
$form.id = createdList.id;
|
||||
const createdList = await lists_create(
|
||||
$formData as List,
|
||||
avatarFile,
|
||||
);
|
||||
$formData.id = createdList.id;
|
||||
}
|
||||
show_toast({
|
||||
type: "success",
|
||||
@@ -135,7 +162,7 @@
|
||||
const response = await r.json();
|
||||
|
||||
searchDropdownItems = response.hits
|
||||
.filter((h: List) => !$form.trails?.includes(h.id!))
|
||||
.filter((h: List) => !$formData.trails?.includes(h.id!))
|
||||
.map((t: Record<string, any>) => ({
|
||||
text: t.name,
|
||||
description: `${t.location ?? "-"}`,
|
||||
@@ -146,30 +173,30 @@
|
||||
|
||||
async function handleSearchClick(item: SearchItem) {
|
||||
const trail = await trails_show(item.value, true);
|
||||
$form.trails?.push(trail.id!);
|
||||
$form.expand = {
|
||||
trails: [...($form.expand?.trails ?? []), trail],
|
||||
list_share_via_list: $form.expand?.list_share_via_list,
|
||||
$formData.trails?.push(trail.id!);
|
||||
$formData.expand = {
|
||||
trails: [...($formData.expand?.trails ?? []), trail],
|
||||
list_share_via_list: $formData.expand?.list_share_via_list,
|
||||
};
|
||||
}
|
||||
|
||||
function deleteTrail(trail: Trail) {
|
||||
$form.trails = $form.trails?.filter((id) => id !== trail.id);
|
||||
$form.expand!.trails = $form.expand!.trails!.filter(
|
||||
$formData.trails = $formData.trails?.filter((id) => id !== trail.id);
|
||||
$formData.expand!.trails = $formData.expand!.trails!.filter(
|
||||
(t) => t.id !== trail.id,
|
||||
);
|
||||
}
|
||||
|
||||
async function findNewTrailShares() {
|
||||
const usersWithAccess: string[] = [
|
||||
$form.author!,
|
||||
...($form.expand?.list_share_via_list ?? []).map((s) => s.user),
|
||||
$formData.author!,
|
||||
...($formData.expand?.list_share_via_list ?? []).map((s) => s.user),
|
||||
];
|
||||
for (const userId of usersWithAccess) {
|
||||
const existingTrailShares = await trail_share_index({
|
||||
user: userId,
|
||||
});
|
||||
for (const trail of $form.expand?.trails ?? []) {
|
||||
for (const trail of $formData.expand?.trails ?? []) {
|
||||
if (
|
||||
trail.author == userId ||
|
||||
trail.author != $currentUser?.id
|
||||
@@ -195,7 +222,7 @@
|
||||
}
|
||||
|
||||
async function publishTrails() {
|
||||
for (const trail of $form.expand?.trails ?? []) {
|
||||
for (const trail of $formData.expand?.trails ?? []) {
|
||||
if (trail.author !== $currentUser?.id) {
|
||||
continue;
|
||||
}
|
||||
@@ -207,27 +234,32 @@
|
||||
function moveTrail(trail: Trail, index: number, direction: 1 | -1) {
|
||||
(document?.activeElement as HTMLElement).blur();
|
||||
|
||||
if (!$form.trails || !$form.expand || !$form.expand.trails) {
|
||||
if (
|
||||
!$formData.trails ||
|
||||
!$formData.expand ||
|
||||
!$formData.expand.trails
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const previousTrail = $form.expand?.trails?.at(index + direction);
|
||||
const previousTrailId = $form.trails?.at(index + direction);
|
||||
const previousTrail = $formData.expand?.trails?.at(index + direction);
|
||||
const previousTrailId = $formData.trails?.at(index + direction);
|
||||
|
||||
if (!previousTrail || !previousTrailId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$form.expand!.trails![index] = previousTrail;
|
||||
$form.expand!.trails![index + direction] = trail;
|
||||
$formData.expand!.trails![index] = previousTrail;
|
||||
$formData.expand!.trails![index + direction] = trail;
|
||||
|
||||
$form.trails[index] = previousTrailId;
|
||||
$form.trails[index + direction] = trail.id!;
|
||||
$formData.trails[index] = previousTrailId;
|
||||
$formData.trails[index + direction] = trail.id!;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title
|
||||
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer</title
|
||||
>{$formData.id ? `${$formData.name} | ${$_("edit")}` : $_("new-list")} |
|
||||
wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
@@ -235,7 +267,7 @@
|
||||
<form
|
||||
id="list-form"
|
||||
class="flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0 overflow-y-scroll"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
<h2 class="text-2xl font-semibold">
|
||||
{$page.params.id === "new" ? $_("new-list") : $_("edit-list")}
|
||||
@@ -270,23 +302,15 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
name="name"
|
||||
label={$_("name")}
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
on:change={handleChange}
|
||||
<TextField name="name" label={$_("name")} error={$errors.name}
|
||||
></TextField>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
label={$_("description")}
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
on:change={handleChange}
|
||||
></Textarea>
|
||||
<Toggle name="public" label={$_("public")} bind:value={$form.public}
|
||||
></Toggle>
|
||||
<Toggle name="public" label={$_("public")}></Toggle>
|
||||
<h3 class="text-xl font-semibold">
|
||||
{$_("trail", { values: { n: 2 } })}
|
||||
</h3>
|
||||
@@ -296,8 +320,8 @@
|
||||
placeholder="{$_('search-trails')}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
{#if $form.expand?.trails?.length}
|
||||
{#each $form.expand?.trails ?? [] as trail, i}
|
||||
{#if $formData.expand?.trails?.length}
|
||||
{#each $formData.expand?.trails ?? [] as trail, i}
|
||||
<div
|
||||
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
|
||||
class:bg-secondary-hover={i == activeTrailIndex}
|
||||
@@ -312,7 +336,7 @@
|
||||
src={trail.photos.length
|
||||
? getFileURL(
|
||||
trail,
|
||||
trail.photos[trail.thumbnail],
|
||||
trail.photos[trail.thumbnail ?? 0],
|
||||
)
|
||||
: "/imgs/default_thumbnail.webp"}
|
||||
alt=""
|
||||
@@ -363,7 +387,7 @@
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</button>
|
||||
{/if}
|
||||
{#if i < $form.expand?.trails?.length - 1}
|
||||
{#if i < $formData.expand?.trails?.length - 1}
|
||||
<button
|
||||
on:click|stopPropagation={() =>
|
||||
moveTrail(trail, i, 1)}
|
||||
@@ -384,7 +408,7 @@
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-center text-sm text-gray-500 my-8"
|
||||
>No routes added</span
|
||||
>{$_("no-routes-added")}</span
|
||||
>
|
||||
{/if}
|
||||
<Button
|
||||
@@ -397,7 +421,7 @@
|
||||
</form>
|
||||
<div id="trail-map" class="max-h-full">
|
||||
<MapWithElevationMaplibre
|
||||
trails={$form.expand?.trails ?? []}
|
||||
trails={$formData.expand?.trails ?? []}
|
||||
bind:activeTrail={activeTrailIndex}
|
||||
bind:this={map}
|
||||
></MapWithElevationMaplibre>
|
||||
|
||||
@@ -22,8 +22,9 @@ export const load: Load = async ({ params, fetch, data }) => {
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof ClientResponseError) {
|
||||
return error(e.status as any, e.message)
|
||||
throw error(e.status as any, e.message)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidate, invalidateAll } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import Button from "$lib/components/base/button.svelte";
|
||||
@@ -10,10 +9,11 @@
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { login } from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { ClientResponseError, type AuthProviderInfo } from "pocketbase";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { object, string } from "yup";
|
||||
import { z } from "zod";
|
||||
|
||||
let loading: boolean = false;
|
||||
|
||||
@@ -31,18 +31,21 @@
|
||||
loginLabel = `${$_("email")}`;
|
||||
}
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<User>({
|
||||
const { form, errors, data } = createForm<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
validationSchema: object<User>({
|
||||
username: string().required($_("required")),
|
||||
password: string().required($_("required")),
|
||||
extend: validator({
|
||||
schema: z.object({
|
||||
username: z.string().min(1, "required"),
|
||||
password: z.string().min(1, "required"),
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (newUser) => {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await login(newUser);
|
||||
window.location.href = $page.url.searchParams.get("r") ?? "/";
|
||||
@@ -80,7 +83,7 @@
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-4 w-[28rem] mt-8"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
{#if $theme == "light"}
|
||||
<LogoTextTwoLineDark></LogoTextTwoLineDark>
|
||||
@@ -93,8 +96,6 @@
|
||||
<TextField
|
||||
name="username"
|
||||
label={loginLabel}
|
||||
bind:value={$form.username}
|
||||
on:change={handleChange}
|
||||
error={$errors.username}
|
||||
></TextField>
|
||||
<div class="flex flex-col">
|
||||
@@ -102,8 +103,6 @@
|
||||
name="password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
error={$errors.password}
|
||||
></TextField>
|
||||
<a
|
||||
|
||||
@@ -8,38 +8,38 @@
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { login, users_create } from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { object, string } from "yup";
|
||||
import { z } from "zod";
|
||||
|
||||
let loading: boolean = false;
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<User>({
|
||||
const { form, errors } = createForm<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
validationSchema: object<User>({
|
||||
username: string()
|
||||
.min(
|
||||
extend: validator({
|
||||
schema: z.object({
|
||||
username: z.string().min(
|
||||
3,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 3 },
|
||||
}),
|
||||
)
|
||||
.required($_("required"))
|
||||
.matches(/^[\w][\w\.]*$/, { message: $_("invalid-username") }),
|
||||
email: string()
|
||||
.email($_("not-a-valid-email-address"))
|
||||
.required($_("required")),
|
||||
password: string()
|
||||
.min(
|
||||
),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "required")
|
||||
.email("not-a-valid-email-address"),
|
||||
password: z.string().min(
|
||||
8,
|
||||
$_("must-be-at-least-n-characters-long", {
|
||||
values: { n: 8 },
|
||||
}),
|
||||
)
|
||||
.required($_("required")),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (newUser) => {
|
||||
loading = true;
|
||||
@@ -77,7 +77,7 @@
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-8 w-[28rem] mt-8"
|
||||
on:submit={handleSubmit}
|
||||
use:form
|
||||
>
|
||||
{#if $theme == "light"}
|
||||
<LogoTextTwoLineDark></LogoTextTwoLineDark>
|
||||
@@ -89,23 +89,14 @@
|
||||
<TextField
|
||||
name="username"
|
||||
label={$_("username")}
|
||||
bind:value={$form.username}
|
||||
on:change={handleChange}
|
||||
error={$errors.username}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="email"
|
||||
label={$_("email")}
|
||||
bind:value={$form.email}
|
||||
on:change={handleChange}
|
||||
error={$errors.email}
|
||||
<TextField name="email" label={$_("email")} error={$errors.email}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
error={$errors.password}
|
||||
></TextField>
|
||||
<Button
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import { page } from "$app/stores";
|
||||
import { type RadioItem } from "$lib/components/base/radio_group.svelte";
|
||||
import { type SearchItem } from "$lib/components/base/search.svelte";
|
||||
import {
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
import { type SelectItem } from "$lib/components/base/select.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import EmailModal from "$lib/components/settings/email_modal.svelte";
|
||||
import PasswordModal from "$lib/components/settings/password_modal.svelte";
|
||||
@@ -48,22 +46,6 @@
|
||||
selectedMapFocus = settings?.mapFocus ?? "trails";
|
||||
});
|
||||
|
||||
async function searchCities(q: string) {
|
||||
const r = await fetch("/api/v1/search/cities500", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ q: q, options: { limit: 5 } }),
|
||||
});
|
||||
const result = await r.json();
|
||||
searchDropdownItems = result.hits.map((h: Record<string, any>) => ({
|
||||
text: h.name,
|
||||
description: `${h.division ? `${h.division} | ` : ""}${
|
||||
country_codes[h["country code"] as keyof typeof country_codes]
|
||||
}`,
|
||||
value: h,
|
||||
icon: "city",
|
||||
}));
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
await users_delete($currentUser!);
|
||||
logout();
|
||||
@@ -73,6 +55,11 @@
|
||||
async function updateEmail(email: string) {
|
||||
try {
|
||||
await users_update({ ...$currentUser!, email: email });
|
||||
show_toast({
|
||||
text: $_("email-updated"),
|
||||
icon: "check",
|
||||
type: "success",
|
||||
});
|
||||
} catch (e) {
|
||||
show_toast({
|
||||
text: "Error updating email",
|
||||
@@ -119,7 +106,9 @@
|
||||
>{$_("change-password")}</button
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xl text-red-400 font-medium">{$_("danger-zone")}</h4>
|
||||
<h4 class="text-xl text-red-400 font-medium">
|
||||
{$_("danger-zone")}
|
||||
</h4>
|
||||
<button class="btn-danger" on:click={openConfirmModal}
|
||||
>{$_("delete-account")}</button
|
||||
>
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
import PhotoPicker from "$lib/components/trail/photo_picker.svelte";
|
||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
||||
import { SummitLogCreateSchema } from "$lib/models/api/summit_log_schema.js";
|
||||
import { TrailCreateSchema } from "$lib/models/api/trail_schema.js";
|
||||
import { WaypointCreateSchema } from "$lib/models/api/waypoint_schema.js";
|
||||
import GPX from "$lib/models/gpx/gpx";
|
||||
import type { List } from "$lib/models/list";
|
||||
import { SummitLog } from "$lib/models/summit_log";
|
||||
@@ -31,7 +34,7 @@
|
||||
trail,
|
||||
trails_create,
|
||||
trails_update,
|
||||
} from "$lib/stores/trail_store";
|
||||
} from "$lib/stores/trail_store.js";
|
||||
import {
|
||||
anchors,
|
||||
appendToRoute,
|
||||
@@ -55,21 +58,21 @@
|
||||
createAnchorMarker,
|
||||
createMarkerFromWaypoint,
|
||||
} from "$lib/util/maplibre_util";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import { createForm } from "felte";
|
||||
import * as M from "maplibre-gl";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { backInOut } from "svelte/easing";
|
||||
import { scale } from "svelte/transition";
|
||||
import { array, number, object, string } from "yup";
|
||||
import { z } from "zod";
|
||||
|
||||
export let data;
|
||||
|
||||
let map: M.Map;
|
||||
|
||||
let mapTrail: Trail[] = [];
|
||||
$: gpxData = $form.expand!?.gpx_data;
|
||||
$: gpxData = $formData.expand!?.gpx_data;
|
||||
$: if (gpxData) {
|
||||
updateTrailOnMap();
|
||||
}
|
||||
@@ -89,20 +92,18 @@
|
||||
let drawingActive = false;
|
||||
let overwriteGPX = false;
|
||||
|
||||
const trailSchema = object<Trail>({
|
||||
id: string().optional(),
|
||||
name: string().required($_("required")),
|
||||
location: string().optional(),
|
||||
distance: number().optional(),
|
||||
difficulty: string()
|
||||
.oneOf(["easy", "moderate", "difficult"])
|
||||
const ClientTrailCreateSchema = TrailCreateSchema.extend({
|
||||
expand: z
|
||||
.object({
|
||||
gpx_data: z.string().optional(),
|
||||
summit_logs: z.array(SummitLogCreateSchema),
|
||||
waypoints: z.array(
|
||||
WaypointCreateSchema.extend({
|
||||
marker: z.any().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
elevation_gain: number().optional(),
|
||||
duration: number().optional(),
|
||||
thumbnail: string().optional(),
|
||||
photos: array(string()).optional(),
|
||||
gpx: string().optional(),
|
||||
description: string().optional(),
|
||||
});
|
||||
|
||||
const modesOfTransport = [
|
||||
@@ -114,7 +115,12 @@
|
||||
|
||||
let autoRouting = true;
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
data: formData,
|
||||
setFields,
|
||||
} = createForm<z.infer<typeof ClientTrailCreateSchema>>({
|
||||
initialValues: {
|
||||
...data.trail,
|
||||
public: data.trail.id
|
||||
@@ -125,28 +131,10 @@
|
||||
$page.data.settings?.category ||
|
||||
$categories[0].id,
|
||||
},
|
||||
validationSchema: trailSchema,
|
||||
onError: (errors) => {
|
||||
if (errors.name) {
|
||||
const nameInput = document.querySelector(
|
||||
"input[name=name]",
|
||||
) as HTMLElement;
|
||||
|
||||
if (window.innerWidth < 768) {
|
||||
window?.scroll({
|
||||
top: nameInput.offsetTop - 24,
|
||||
behavior: "smooth",
|
||||
});
|
||||
} else {
|
||||
const form = document.getElementById("trail-form");
|
||||
form?.scroll({
|
||||
top: nameInput.offsetTop - 130,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onSubmit: async (submittedTrail) => {
|
||||
extend: validator({
|
||||
schema: ClientTrailCreateSchema,
|
||||
}),
|
||||
onSubmit: async (form) => {
|
||||
loading = true;
|
||||
try {
|
||||
const htmlForm = document.getElementById(
|
||||
@@ -154,43 +142,43 @@
|
||||
) as HTMLFormElement;
|
||||
const formData = new FormData(htmlForm);
|
||||
if (!formData.get("public")) {
|
||||
submittedTrail.public = false;
|
||||
form.public = false;
|
||||
}
|
||||
submittedTrail.photos = submittedTrail.photos.filter(
|
||||
form.photos = form.photos.filter(
|
||||
(p) => !p.startsWith("data:image/svg+xml;base64"),
|
||||
);
|
||||
|
||||
if ($form.expand!.gpx_data && overwriteGPX) {
|
||||
gpxFile = new Blob([$form.expand!.gpx_data], {
|
||||
if ($formData.expand!.gpx_data && overwriteGPX) {
|
||||
gpxFile = new Blob([$formData.expand!.gpx_data], {
|
||||
type: "text/xml",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(!$form.lat || !$form.lon) &&
|
||||
(!$formData.lat || !$formData.lon) &&
|
||||
route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)
|
||||
) {
|
||||
$form.lat = route.trk
|
||||
$formData.lat = route.trk
|
||||
?.at(0)
|
||||
?.trkseg?.at(0)
|
||||
?.trkpt?.at(0)?.$.lat;
|
||||
$form.lon = route.trk
|
||||
$formData.lon = route.trk
|
||||
?.at(0)
|
||||
?.trkseg?.at(0)
|
||||
?.trkpt?.at(0)?.$.lon;
|
||||
}
|
||||
|
||||
if (!submittedTrail.id) {
|
||||
if (!form.id) {
|
||||
const createdTrail = await trails_create(
|
||||
submittedTrail,
|
||||
form as Trail,
|
||||
photoFiles,
|
||||
gpxFile,
|
||||
);
|
||||
$form.id = createdTrail.id;
|
||||
$formData.id = createdTrail.id;
|
||||
} else {
|
||||
await trails_update(
|
||||
$trail,
|
||||
submittedTrail,
|
||||
form as Trail,
|
||||
photoFiles,
|
||||
gpxFile,
|
||||
);
|
||||
@@ -219,8 +207,8 @@
|
||||
clearAnchorMarker();
|
||||
clearRoute();
|
||||
|
||||
if ($form.expand!.gpx_data) {
|
||||
const gpx = await GPX.parse($form.expand!.gpx_data);
|
||||
if ($formData.expand!.gpx_data) {
|
||||
const gpx = await GPX.parse($formData.expand!.gpx_data);
|
||||
if (!(gpx instanceof Error)) {
|
||||
setRoute(gpx);
|
||||
initRouteAnchors(gpx);
|
||||
@@ -251,19 +239,27 @@
|
||||
gpxFile = file;
|
||||
|
||||
try {
|
||||
const prevId = $form.id;
|
||||
const prevId = $formData.id;
|
||||
const parseResult = await gpx2trail(gpxData, selectedFile.name);
|
||||
$form = parseResult.trail;
|
||||
$form.id = prevId;
|
||||
$form.expand!.gpx_data = gpxData;
|
||||
$form.category = $page.data.settings.category || $categories[0].id;
|
||||
$form.public = $page.data.settings?.privacy.trails === "public";
|
||||
setFields(parseResult.trail);
|
||||
$formData.id = prevId;
|
||||
$formData.expand!.gpx_data = gpxData;
|
||||
setFields(
|
||||
"category",
|
||||
$page.data.settings.category || $categories[0].id,
|
||||
);
|
||||
setFields(
|
||||
"public",
|
||||
$page.data.settings?.privacy.trails === "public",
|
||||
);
|
||||
|
||||
const log = new SummitLog(parseResult.trail.date as string, {
|
||||
distance: $form.distance,
|
||||
elevation_gain: $form.elevation_gain,
|
||||
elevation_loss: $form.elevation_loss,
|
||||
duration: $form.duration ? $form.duration * 60 : undefined,
|
||||
distance: $formData.distance,
|
||||
elevation_gain: $formData.elevation_gain,
|
||||
elevation_loss: $formData.elevation_loss,
|
||||
duration: $formData.duration
|
||||
? $formData.duration * 60
|
||||
: undefined,
|
||||
});
|
||||
|
||||
log.expand!.gpx_data = gpxData;
|
||||
@@ -272,16 +268,16 @@
|
||||
type: selectedFile.type,
|
||||
});
|
||||
|
||||
$form.expand!.summit_logs.push(log);
|
||||
$formData.expand!.summit_logs.push(log);
|
||||
|
||||
setRoute(parseResult.gpx);
|
||||
initRouteAnchors(parseResult.gpx);
|
||||
|
||||
for (const waypoint of $form.expand!.waypoints) {
|
||||
for (const waypoint of $formData.expand!.waypoints) {
|
||||
saveWaypoint(waypoint);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
|
||||
show_toast({
|
||||
icon: "close",
|
||||
@@ -295,23 +291,25 @@
|
||||
body: JSON.stringify({
|
||||
q: "",
|
||||
options: {
|
||||
filter: [`_geoRadius(${$form.lat}, ${$form.lon}, 10000)`],
|
||||
sort: [`_geoPoint(${$form.lat}, ${$form.lon}):asc`],
|
||||
filter: [
|
||||
`_geoRadius(${$formData.lat}, ${$formData.lon}, 10000)`,
|
||||
],
|
||||
sort: [`_geoPoint(${$formData.lat}, ${$formData.lon}):asc`],
|
||||
limit: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const closestCity = (await r.json()).hits[0];
|
||||
|
||||
$form.location = closestCity.name;
|
||||
setFields("location", closestCity.name);
|
||||
}
|
||||
|
||||
function clearWaypoints() {
|
||||
for (const waypoint of $form.expand!.waypoints) {
|
||||
for (const waypoint of $formData.expand!.waypoints) {
|
||||
waypoint.marker?.remove();
|
||||
}
|
||||
$form.expand!.waypoints = [];
|
||||
$form.waypoints = [];
|
||||
$formData.expand!.waypoints = [];
|
||||
$formData.waypoints = [];
|
||||
}
|
||||
|
||||
function clearAnchorMarker() {
|
||||
@@ -365,23 +363,23 @@
|
||||
}
|
||||
|
||||
function deleteWaypoint(index: number) {
|
||||
$form.expand!.waypoints.splice(index, 1);
|
||||
$form.waypoints.splice(index, 1);
|
||||
$form.expand!.waypoints = $form.expand!.waypoints;
|
||||
$formData.expand!.waypoints.splice(index, 1);
|
||||
$formData.waypoints.splice(index, 1);
|
||||
$formData.expand!.waypoints = $formData.expand!.waypoints;
|
||||
}
|
||||
|
||||
function saveWaypoint(savedWaypoint: Waypoint) {
|
||||
let editedWaypointIndex = $form.expand!.waypoints.findIndex(
|
||||
let editedWaypointIndex = $formData.expand!.waypoints.findIndex(
|
||||
(s) => s.id == savedWaypoint.id,
|
||||
);
|
||||
|
||||
if (editedWaypointIndex >= 0) {
|
||||
$form.expand!.waypoints[editedWaypointIndex].marker?.remove();
|
||||
$form.expand!.waypoints[editedWaypointIndex] = savedWaypoint;
|
||||
$formData.expand!.waypoints[editedWaypointIndex].marker?.remove();
|
||||
$formData.expand!.waypoints[editedWaypointIndex] = savedWaypoint;
|
||||
} else {
|
||||
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
||||
$form.expand!.waypoints = [
|
||||
...$form.expand!.waypoints,
|
||||
$formData.expand!.waypoints = [
|
||||
...$formData.expand!.waypoints,
|
||||
savedWaypoint,
|
||||
];
|
||||
}
|
||||
@@ -393,16 +391,17 @@
|
||||
|
||||
function moveMarker(marker: M.Marker, wpId?: string) {
|
||||
const position = marker.getLngLat();
|
||||
const editableWaypointIndex = $form.expand!.waypoints.findIndex(
|
||||
const editableWaypointIndex = $formData.expand!.waypoints.findIndex(
|
||||
(w) => w.id == wpId,
|
||||
);
|
||||
const editableWaypoint = $form.expand!.waypoints[editableWaypointIndex];
|
||||
const editableWaypoint =
|
||||
$formData.expand!.waypoints[editableWaypointIndex];
|
||||
if (!editableWaypoint) {
|
||||
return;
|
||||
}
|
||||
editableWaypoint.lat = position.lat;
|
||||
editableWaypoint.lon = position.lng;
|
||||
$form.expand!.waypoints = [...$form.expand!.waypoints];
|
||||
$formData.expand!.waypoints = [...$formData.expand!.waypoints];
|
||||
}
|
||||
|
||||
function beforeSummitLogModalOpen() {
|
||||
@@ -413,16 +412,17 @@
|
||||
function saveSummitLog(e: CustomEvent<SummitLog>) {
|
||||
const savedSummitLog = e.detail;
|
||||
|
||||
let editedSummitLogIndex = $form.expand!.summit_logs.findIndex(
|
||||
let editedSummitLogIndex = $formData.expand!.summit_logs.findIndex(
|
||||
(s) => s.id == savedSummitLog.id,
|
||||
);
|
||||
|
||||
if (editedSummitLogIndex >= 0) {
|
||||
$form.expand!.summit_logs[editedSummitLogIndex] = savedSummitLog;
|
||||
$formData.expand!.summit_logs[editedSummitLogIndex] =
|
||||
savedSummitLog;
|
||||
} else {
|
||||
savedSummitLog.id = cryptoRandomString({ length: 15 });
|
||||
$form.expand!.summit_logs = [
|
||||
...$form.expand!.summit_logs,
|
||||
$formData.expand!.summit_logs = [
|
||||
...$formData.expand!.summit_logs,
|
||||
savedSummitLog,
|
||||
];
|
||||
}
|
||||
@@ -437,21 +437,21 @@
|
||||
summitLog.set(currentSummitLog);
|
||||
openSummitLogModal();
|
||||
} else if (e.detail.value === "delete") {
|
||||
$form.expand!.summit_logs.splice(index, 1);
|
||||
$form.summit_logs.splice(index, 1);
|
||||
$form.expand!.summit_logs = $form.expand!.summit_logs;
|
||||
$formData.expand!.summit_logs.splice(index, 1);
|
||||
$formData.summit_logs.splice(index, 1);
|
||||
$formData.expand!.summit_logs = $formData.expand!.summit_logs;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleListSelection(list: List) {
|
||||
if (!$form.id) {
|
||||
if (!$formData.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (list.trails?.includes($form.id!)) {
|
||||
list = await lists_remove_trail(list, $form);
|
||||
if (list.trails?.includes($formData.id!)) {
|
||||
list = await lists_remove_trail(list, $formData as Trail);
|
||||
} else {
|
||||
list = await lists_add_trail(list, $form);
|
||||
list = await lists_add_trail(list, $formData as Trail);
|
||||
}
|
||||
const index = data.lists.items.findIndex((l) => l.id == list.id);
|
||||
if (index >= 0) {
|
||||
@@ -615,21 +615,22 @@
|
||||
function updateTrailWithRouteData() {
|
||||
overwriteGPX = true;
|
||||
const totals = route.getTotals();
|
||||
$form.distance = totals.distance;
|
||||
$form.duration = totals.duration;
|
||||
$form.elevation_gain = totals.elevationGain;
|
||||
$form.elevation_loss = totals.elevationLoss;
|
||||
$form.expand!.gpx_data = route.toString();
|
||||
$formData.distance = totals.distance;
|
||||
$formData.duration = totals.duration;
|
||||
$formData.elevation_gain = totals.elevationGain;
|
||||
$formData.elevation_loss = totals.elevationLoss;
|
||||
$formData.expand!.gpx_data = route.toString();
|
||||
}
|
||||
|
||||
function updateTrailOnMap() {
|
||||
mapTrail = [{ ...$form }];
|
||||
mapTrail = [{ ...($formData as Trail) }];
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title
|
||||
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-trail")} | wanderer</title
|
||||
>{$formData.id ? `${$formData.name} | ${$_("edit")}` : $_("new-trail")} |
|
||||
wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
@@ -637,7 +638,7 @@
|
||||
<form
|
||||
id="trail-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}
|
||||
use:form
|
||||
>
|
||||
<h3 class="text-xl font-semibold">{$_("pick-a-trail")}</h3>
|
||||
<Button
|
||||
@@ -683,24 +684,27 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 justify-around">
|
||||
<fieldset
|
||||
class="grid grid-cols-2 gap-4 justify-around"
|
||||
data-felte-keep-on-remove
|
||||
>
|
||||
{#if editingBasicInfo}
|
||||
<TextField
|
||||
bind:value={$form.distance}
|
||||
bind:value={$formData.distance}
|
||||
name="distance"
|
||||
label={$_("distance")}
|
||||
></TextField>
|
||||
<TextField
|
||||
bind:value={$form.duration}
|
||||
bind:value={$formData.duration}
|
||||
name="duration"
|
||||
label={$_("est-duration")}
|
||||
></TextField><TextField
|
||||
bind:value={$form.elevation_gain}
|
||||
bind:value={$formData.elevation_gain}
|
||||
name="elevation_gain"
|
||||
label={$_("elevation-gain")}
|
||||
></TextField>
|
||||
<TextField
|
||||
bind:value={$form.elevation_loss}
|
||||
bind:value={$formData.elevation_loss}
|
||||
name="elevation_loss"
|
||||
label={$_("elevation-loss")}
|
||||
></TextField>
|
||||
@@ -708,75 +712,63 @@
|
||||
<div>
|
||||
<p>{$_("distance")}</p>
|
||||
<span class="font-medium"
|
||||
>{formatDistance($form.distance)}</span
|
||||
>{formatDistance($formData.distance)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="distance"
|
||||
value={$form.distance}
|
||||
value={$formData.distance}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>{$_("est-duration")}</p>
|
||||
<span class="font-medium"
|
||||
>{formatTimeHHMM($form.duration)}</span
|
||||
>{formatTimeHHMM($formData.duration)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="duration"
|
||||
value={$form.duration}
|
||||
value={$formData.duration}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>{$_("elevation-gain")}</p>
|
||||
<span class="font-medium"
|
||||
>{formatElevation($form.elevation_gain)}</span
|
||||
>{formatElevation($formData.elevation_gain)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="elevation_gain"
|
||||
value={$form.elevation_gain}
|
||||
value={$formData.elevation_gain}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>{$_("elevation-loss")}</p>
|
||||
<span class="font-medium"
|
||||
>{formatElevation($form.elevation_loss)}</span
|
||||
>{formatElevation($formData.elevation_loss)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="elevation_gain"
|
||||
value={$form.elevation_gain}
|
||||
value={$formData.elevation_gain}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<input type="hidden" name="lat" value={$form.lat} />
|
||||
<input type="hidden" name="lon" value={$form.lon} />
|
||||
</div>
|
||||
<TextField
|
||||
name="name"
|
||||
label={$_("name")}
|
||||
on:change={handleChange}
|
||||
error={$errors.name}
|
||||
bind:value={$form.name}
|
||||
</fieldset>
|
||||
<TextField name="name" label={$_("name")} error={$errors.name}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="location"
|
||||
label={$_("location")}
|
||||
error={$errors.location}
|
||||
bind:value={$form.location}
|
||||
></TextField>
|
||||
<Datepicker label={$_("date")} bind:value={$form.date}></Datepicker>
|
||||
<Textarea
|
||||
name="description"
|
||||
label={$_("describe-your-trail")}
|
||||
bind:value={$form.description}
|
||||
<Datepicker label={$_("date")} bind:value={$formData.date}></Datepicker>
|
||||
<Textarea name="description" label={$_("describe-your-trail")}
|
||||
></Textarea>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-4">
|
||||
<Select
|
||||
name="difficulty"
|
||||
label={$_("difficulty")}
|
||||
bind:value={$form.difficulty}
|
||||
items={[
|
||||
{ text: $_("easy"), value: "easy" },
|
||||
{ text: $_("moderate"), value: "moderate" },
|
||||
@@ -786,7 +778,6 @@
|
||||
<Select
|
||||
name="category"
|
||||
label={$_("category")}
|
||||
bind:value={$form.category}
|
||||
items={$categories.map((c) => ({
|
||||
text: $_(c.name),
|
||||
value: c.id,
|
||||
@@ -794,14 +785,13 @@
|
||||
></Select>
|
||||
</div>
|
||||
|
||||
<Toggle name="public" label={$_("public")} bind:value={$form.public}
|
||||
></Toggle>
|
||||
<Toggle name="public" label={$_("public")}></Toggle>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">
|
||||
{$_("waypoints", { values: { n: 2 } })}
|
||||
</h3>
|
||||
<ul>
|
||||
{#each $form.expand?.waypoints ?? [] as waypoint, i}
|
||||
{#each $formData.expand?.waypoints ?? [] as waypoint, i}
|
||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||
<WaypointCard
|
||||
{waypoint}
|
||||
@@ -822,15 +812,15 @@
|
||||
<h3 class="text-xl font-semibold">{$_("photos")}</h3>
|
||||
<PhotoPicker
|
||||
id="trail"
|
||||
parent={$form}
|
||||
bind:photos={$form.photos}
|
||||
bind:thumbnail={$form.thumbnail}
|
||||
parent={$formData}
|
||||
bind:photos={$formData.photos}
|
||||
bind:thumbnail={$formData.thumbnail}
|
||||
bind:photoFiles
|
||||
></PhotoPicker>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">{$_("summit-book")}</h3>
|
||||
<ul>
|
||||
{#each $form.expand?.summit_logs ?? [] as log, i}
|
||||
{#each $formData.expand?.summit_logs ?? [] as log, i}
|
||||
<li>
|
||||
<SummitLogCard
|
||||
{log}
|
||||
@@ -853,7 +843,7 @@
|
||||
</h3>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
{#each data.lists.items as list}
|
||||
{#if $form.id && list.trails?.includes($form.id)}
|
||||
{#if $formData.id && list.trails?.includes($formData.id)}
|
||||
<div
|
||||
class="flex gap-2 items-center border border-input-border rounded-xl p-2"
|
||||
>
|
||||
@@ -873,7 +863,7 @@
|
||||
<Button
|
||||
secondary={true}
|
||||
tooltip={$_("save-your-trail-first")}
|
||||
disabled={!$form.id}
|
||||
disabled={!$formData.id}
|
||||
type="button"
|
||||
on:click={openListSelectModal}
|
||||
><i class="fa fa-plus mr-2"></i>{$_("add-to-list")}</Button
|
||||
|
||||
Reference in New Issue
Block a user