adds tags
This commit is contained in:
@@ -130,7 +130,7 @@ func createTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := util.IndexTrail(record, author, client); err != nil {
|
if err := util.IndexTrail(e.App, record, author, client); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ func updateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = util.UpdateTrail(record, author, client)
|
err = util.UpdateTrail(e.App, record, author, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -789,7 +789,7 @@ func bootstrapMeilisearchTrails(app core.App, client meilisearch.ServiceManager)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := util.IndexTrail(trail, author, client); err != nil {
|
if err := util.IndexTrail(app, trail, author, client); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
|
_, err = client.Index("trails").UpdateFilterableAttributes(&[]string{
|
||||||
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares",
|
"_geo", "author", "category", "completed", "date", "difficulty", "distance", "elevation_gain", "elevation_loss", "public", "shares", "tags",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
89
db/migrations/1742409454_created_tags.go
Normal file
89
db/migrations/1742409454_created_tags.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
jsonData := `{
|
||||||
|
"createRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 15,
|
||||||
|
"min": 15,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1579384326",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "autodate2990389176",
|
||||||
|
"name": "created",
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": false,
|
||||||
|
"presentable": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "autodate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "autodate3332085495",
|
||||||
|
"name": "updated",
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": true,
|
||||||
|
"presentable": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "autodate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "pbc_1219621782",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "tags",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
}`
|
||||||
|
|
||||||
|
collection := &core.Collection{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1219621782")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Delete(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
44
db/migrations/1742411241_updated_tags.go
Normal file
44
db/migrations/1742411241_updated_tags.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1219621782")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"createRule": "@request.auth.id != \"\"",
|
||||||
|
"listRule": "",
|
||||||
|
"viewRule": ""
|
||||||
|
}`), &collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1219621782")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"createRule": null,
|
||||||
|
"listRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
}`), &collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
42
db/migrations/1742411270_updated_tags.go
Normal file
42
db/migrations/1742411270_updated_tags.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1219621782")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"listRule": "@request.auth.id != \"\"",
|
||||||
|
"viewRule": "@request.auth.id != \"\""
|
||||||
|
}`), &collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1219621782")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"listRule": "",
|
||||||
|
"viewRule": ""
|
||||||
|
}`), &collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
44
db/migrations/1742412356_updated_trails.go
Normal file
44
db/migrations/1742412356_updated_trails.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add field
|
||||||
|
if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1219621782",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation1874629670",
|
||||||
|
"maxSelect": 999,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "tags",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}`)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.Fields.RemoveById("relation1874629670")
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/meilisearch/meilisearch-go"
|
"github.com/meilisearch/meilisearch-go"
|
||||||
@@ -19,6 +20,13 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
|
|||||||
thumbnail = photos[thumbnailIndex]
|
thumbnail = photos[thumbnailIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagRecords := r.ExpandedAll("tags")
|
||||||
|
tags := make([]string, len(tagRecords))
|
||||||
|
|
||||||
|
for i, v := range tagRecords {
|
||||||
|
tags[i] = v.GetString("name")
|
||||||
|
}
|
||||||
|
|
||||||
document := map[string]interface{}{
|
document := map[string]interface{}{
|
||||||
"id": r.Id,
|
"id": r.Id,
|
||||||
"author": r.GetString("author"),
|
"author": r.GetString("author"),
|
||||||
@@ -39,6 +47,7 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
|
|||||||
"public": r.GetBool("public"),
|
"public": r.GetBool("public"),
|
||||||
"thumbnail": thumbnail,
|
"thumbnail": thumbnail,
|
||||||
"gpx": r.GetString("gpx"),
|
"gpx": r.GetString("gpx"),
|
||||||
|
"tags": tags,
|
||||||
"_geo": map[string]float64{
|
"_geo": map[string]float64{
|
||||||
"lat": r.GetFloat("lat"),
|
"lat": r.GetFloat("lat"),
|
||||||
"lng": r.GetFloat("lon"),
|
"lng": r.GetFloat("lon"),
|
||||||
@@ -70,7 +79,12 @@ func documentFromListRecord(r *core.Record, includeShares bool) map[string]inter
|
|||||||
return document
|
return document
|
||||||
}
|
}
|
||||||
|
|
||||||
func IndexTrail(r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
|
func IndexTrail(app core.App, r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
|
||||||
|
errs := app.ExpandRecord(r, []string{"tags"}, nil)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("failed to expand: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
documents := []map[string]interface{}{documentFromTrailRecord(r, author, true)}
|
documents := []map[string]interface{}{documentFromTrailRecord(r, author, true)}
|
||||||
|
|
||||||
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
|
if _, err := client.Index("trails").AddDocuments(documents); err != nil {
|
||||||
@@ -80,7 +94,12 @@ func IndexTrail(r *core.Record, author *core.Record, client meilisearch.ServiceM
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateTrail(r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
|
func UpdateTrail(app core.App, r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
|
||||||
|
errs := app.ExpandRecord(r, []string{"tags"}, nil)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("failed to expand: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
documents := documentFromTrailRecord(r, author, false)
|
documents := documentFromTrailRecord(r, author, false)
|
||||||
|
|
||||||
if _, err := client.Index("trails").UpdateDocuments(documents); err != nil {
|
if _, err := client.Index("trails").UpdateDocuments(documents); err != nil {
|
||||||
|
|||||||
30
web/src/lib/components/base/chip.svelte
Normal file
30
web/src/lib/components/base/chip.svelte
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { MouseEventHandler } from "svelte/elements";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
primary?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { text, primary = true, closable = false, onclick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="{primary
|
||||||
|
? 'bg-primary text-white'
|
||||||
|
: 'bg-background-inverse/10'} px-2 py-1 rounded-full flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span class="text-sm">{text}</span>
|
||||||
|
{#if closable}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Close"
|
||||||
|
{onclick}
|
||||||
|
class="text-white hover:bg-primary-hover rounded-full w-4 h-4 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="fa fa-close text-xs"></i>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -2,50 +2,63 @@
|
|||||||
export type ComboboxItem = {
|
export type ComboboxItem = {
|
||||||
text: string;
|
text: string;
|
||||||
value: any;
|
value: any;
|
||||||
icon: string;
|
icon?: string;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ChangeEventHandler } from "svelte/elements";
|
import type { ChangeEventHandler } from "svelte/elements";
|
||||||
import TextField from "./text_field.svelte";
|
import Chip from "./chip.svelte";
|
||||||
import { tick } from "svelte";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string;
|
value?: string | ComboboxItem[];
|
||||||
items?: ComboboxItem[];
|
items?: ComboboxItem[];
|
||||||
|
multiple?: boolean;
|
||||||
|
chips?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
extraClasses?: string;
|
extraClasses?: string;
|
||||||
onchange?: ChangeEventHandler<HTMLInputElement>;
|
onchange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
onupdate?: (q: string) => void;
|
onupdate?: (q: string) => void;
|
||||||
onclick?: (item: ComboboxItem) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
name = "",
|
name = "",
|
||||||
icon = "",
|
icon = "",
|
||||||
label = "",
|
label = "",
|
||||||
value = $bindable(""),
|
multiple = false,
|
||||||
|
value = $bindable(multiple ? [] : undefined),
|
||||||
items = [],
|
items = [],
|
||||||
|
chips = false,
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
extraClasses = "",
|
extraClasses = "",
|
||||||
onchange,
|
onchange,
|
||||||
onupdate,
|
onupdate,
|
||||||
onclick,
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let searching: boolean = $state(false);
|
let searching: boolean = $state(false);
|
||||||
|
|
||||||
|
let inputValue: string = $state("");
|
||||||
|
|
||||||
let dropDownOpen = $derived(
|
let dropDownOpen = $derived(
|
||||||
value.length > 0 && items.length > 0 && searching,
|
((multiple && inputValue.length > 0) ||
|
||||||
|
(!multiple && (value as string)?.length > 0)) &&
|
||||||
|
items.length > 0 &&
|
||||||
|
searching,
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onSearchType() {
|
$effect(() => {
|
||||||
await tick();
|
items;
|
||||||
|
makeMatchesBold();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function makeMatchesBold() {
|
||||||
const dropdownMenu = document.querySelector(".menu");
|
const dropdownMenu = document.querySelector(".menu");
|
||||||
|
|
||||||
|
const relevantValue = multiple ? inputValue : (value as string);
|
||||||
|
|
||||||
if (dropdownMenu) {
|
if (dropdownMenu) {
|
||||||
for (let i = 0; i < dropdownMenu.children.length; i++) {
|
for (let i = 0; i < dropdownMenu.children.length; i++) {
|
||||||
const li = dropdownMenu.children[i];
|
const li = dropdownMenu.children[i];
|
||||||
@@ -53,14 +66,18 @@
|
|||||||
textNode.innerHTML = items[i].text;
|
textNode.innerHTML = items[i].text;
|
||||||
|
|
||||||
const text = textNode.innerText.replace(
|
const text = textNode.innerText.replace(
|
||||||
new RegExp(value, "gi"),
|
new RegExp(relevantValue, "gi"),
|
||||||
(match) => `<strong>${match}</strong>`,
|
(match) => `<strong>${match}</strong>`,
|
||||||
);
|
);
|
||||||
textNode.innerHTML = text;
|
textNode.innerHTML = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update(value);
|
async function onSearchType() {
|
||||||
|
const relevantValue = multiple ? inputValue : (value as string);
|
||||||
|
|
||||||
|
update(relevantValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(q: string) {
|
function update(q: string) {
|
||||||
@@ -69,25 +86,117 @@
|
|||||||
|
|
||||||
function handleItemClick(e: Event, item: ComboboxItem) {
|
function handleItemClick(e: Event, item: ComboboxItem) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (multiple) {
|
||||||
|
if ((value as ComboboxItem[]).some((i) => i.text == item.text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inputValue = "";
|
||||||
|
value = [...(value as ComboboxItem[]), item];
|
||||||
|
} else {
|
||||||
value = item.value;
|
value = item.value;
|
||||||
onclick?.(item);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (!multiple) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!inputValue.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((value as ComboboxItem[]).some((i) => i.text == inputValue)) {
|
||||||
|
inputValue = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingItemFromSuggestions = items.find(
|
||||||
|
(i) => i.text == inputValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
value = [
|
||||||
|
...(value as ComboboxItem[]),
|
||||||
|
matchingItemFromSuggestions
|
||||||
|
? matchingItemFromSuggestions
|
||||||
|
: {
|
||||||
|
text: inputValue,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
inputValue = "";
|
||||||
|
} else if (e.key == "Backspace" && inputValue.length == 0) {
|
||||||
|
value = (value as ComboboxItem[]).filter(
|
||||||
|
(_, i) => i !== value.length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputValue() {
|
||||||
|
if (multiple) {
|
||||||
|
return inputValue;
|
||||||
|
} else {
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInputValue(v: string) {
|
||||||
|
if (multiple) {
|
||||||
|
inputValue = v;
|
||||||
|
} else {
|
||||||
|
value = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative {extraClasses}">
|
<div class="relative {extraClasses}">
|
||||||
<TextField
|
{#if label.length}
|
||||||
|
<label for={name} class="text-sm font-medium pb-1">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1 flex-wrap bg-input-background border border-input-border rounded-md p-3 transition-colors focus-within:border-input-border-focus focus-within:ring-0 w-full {extraClasses}"
|
||||||
|
>
|
||||||
|
{#if multiple}
|
||||||
|
{#each value as ComboboxItem[] as v, i}
|
||||||
|
{#if chips}
|
||||||
|
<Chip
|
||||||
|
text={v.text}
|
||||||
|
closable
|
||||||
|
onclick={(e) => {
|
||||||
|
value = (value as ComboboxItem[]).filter(
|
||||||
|
(_, idx) => i != idx,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></Chip>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
>{v.text}{i < (value as ComboboxItem[]).length - 1
|
||||||
|
? ","
|
||||||
|
: ""}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
class="flex-1 min-w-24 bg-input-background focus:outline-none"
|
||||||
type="search"
|
type="search"
|
||||||
{name}
|
{name}
|
||||||
autocomplete="off"
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
{placeholder}
|
|
||||||
bind:value
|
|
||||||
{onchange}
|
|
||||||
oninput={onSearchType}
|
oninput={onSearchType}
|
||||||
|
autocomplete="off"
|
||||||
|
{onchange}
|
||||||
|
placeholder={value.length ? undefined : placeholder}
|
||||||
onfocusin={() => (searching = true)}
|
onfocusin={() => (searching = true)}
|
||||||
onfocusout={() => (searching = false)}
|
onfocusout={() => (searching = false)}
|
||||||
></TextField>
|
onkeydown={(e) => handleKeydown(e)}
|
||||||
|
bind:value={getInputValue, setInputValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if dropDownOpen}
|
{#if dropDownOpen}
|
||||||
<ul
|
<ul
|
||||||
@@ -104,8 +213,9 @@
|
|||||||
onmousedown={(e) => handleItemClick(e, item)}
|
onmousedown={(e) => handleItemClick(e, item)}
|
||||||
onkeydown={(e) => handleItemClick(e, item)}
|
onkeydown={(e) => handleItemClick(e, item)}
|
||||||
>
|
>
|
||||||
|
{#if item.icon}
|
||||||
<i class="fa fa-{item.icon} mr-6"></i>
|
<i class="fa fa-{item.icon} mr-6"></i>
|
||||||
|
{/if}
|
||||||
<p class="text-ellipsis">{item.text}</p>
|
<p class="text-ellipsis">{item.text}</p>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import type { SelectItem } from "./select.svelte";
|
import type { SelectItem } from "./select.svelte";
|
||||||
|
import Chip from "./chip.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items?: SelectItem[];
|
items?: SelectItem[];
|
||||||
@@ -58,18 +59,11 @@
|
|||||||
<span class="text-gray-400">{placeholder}</span>
|
<span class="text-gray-400">{placeholder}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#each value as item}
|
{#each value as item}
|
||||||
<div
|
<Chip
|
||||||
class="bg-primary text-white px-2 py-1 rounded-full flex items-center gap-1"
|
text={$_(item.text)}
|
||||||
>
|
closable
|
||||||
<span class="text-sm">{$_(item.text)}</span>
|
|
||||||
<button
|
|
||||||
aria-label="Close"
|
|
||||||
onclick={(e) => removeItem(e, item)}
|
onclick={(e) => removeItem(e, item)}
|
||||||
class="text-white hover:bg-primary-hover rounded-full w-4 h-4 flex items-center justify-center"
|
></Chip>
|
||||||
>
|
|
||||||
<i class="fa fa-close"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
<i
|
<i
|
||||||
class="fa fa-caret-down absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 transition-transform"
|
class="fa fa-caret-down absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 transition-transform"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import ShareInfo from "../share_info.svelte";
|
import ShareInfo from "../share_info.svelte";
|
||||||
import type { MouseEventHandler } from "svelte/elements";
|
import type { MouseEventHandler } from "svelte/elements";
|
||||||
|
import Chip from "../base/chip.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trail: Trail;
|
trail: Trail;
|
||||||
@@ -124,6 +125,13 @@
|
|||||||
{trail.expand.author.username}
|
{trail.expand.author.username}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if trail.tags?.length}
|
||||||
|
<div class="flex flex-wrap gap-1 mb-3">
|
||||||
|
{#each trail.tags ?? [] as t}
|
||||||
|
<Chip text={t} closable={false} primary={false}></Chip>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex gap-x-4">
|
<div class="flex gap-x-4">
|
||||||
{#if trail.location}
|
{#if trail.location}
|
||||||
<h5>
|
<h5>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
import { searchLocations } from "$lib/stores/search_store";
|
import { searchLocations } from "$lib/stores/search_store";
|
||||||
import { getIconForLocation } from "$lib/util/icon_util";
|
import { getIconForLocation } from "$lib/util/icon_util";
|
||||||
|
import { tags_index } from "$lib/stores/tag_store";
|
||||||
|
import Combobox, { type ComboboxItem } from "../base/combobox.svelte";
|
||||||
|
import { T } from "@threlte/core";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
@@ -59,6 +62,8 @@
|
|||||||
|
|
||||||
let citySearchQuery: string = $state("");
|
let citySearchQuery: string = $state("");
|
||||||
|
|
||||||
|
let tagItems: ComboboxItem[] = $state([]);
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
onupdate?.(filter);
|
onupdate?.(filter);
|
||||||
}
|
}
|
||||||
@@ -133,6 +138,20 @@
|
|||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function searchTags(q: string) {
|
||||||
|
const result = await tags_index(q);
|
||||||
|
tagItems = result.items.map((t) => ({ text: t.name, value: t }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterTags(): ComboboxItem[] {
|
||||||
|
return filter.tags.map((t) => ({ text: t, value: t }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFilterTags(tags: ComboboxItem[]) {
|
||||||
|
filter.tags = tags.map((t) => t.text);
|
||||||
|
update();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="trail-filter p-8 border border-input-border rounded-xl">
|
<div class="trail-filter p-8 border border-input-border rounded-xl">
|
||||||
@@ -169,6 +188,17 @@
|
|||||||
placeholder={`${$_("filter-categories")}...`}
|
placeholder={`${$_("filter-categories")}...`}
|
||||||
></MultiSelect>
|
></MultiSelect>
|
||||||
<hr class="my-4 border-separator" />
|
<hr class="my-4 border-separator" />
|
||||||
|
<Combobox
|
||||||
|
bind:value={getFilterTags, setFilterTags}
|
||||||
|
onupdate={searchTags}
|
||||||
|
placeholder={`${$_("filter-tags")}...`}
|
||||||
|
items={tagItems}
|
||||||
|
label={$_("tags")}
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
></Combobox>
|
||||||
|
<hr class="my-4 border-separator" />
|
||||||
|
|
||||||
{#if pb.authStore.record}
|
{#if pb.authStore.record}
|
||||||
<UserSearch
|
<UserSearch
|
||||||
onclick={(item) => setAuthorFilter(item)}
|
onclick={(item) => setAuthorFilter(item)}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
import SummitLogTable from "../summit_log/summit_log_table.svelte";
|
import SummitLogTable from "../summit_log/summit_log_table.svelte";
|
||||||
import MapWithElevationMaplibre from "./map_with_elevation_maplibre.svelte";
|
import MapWithElevationMaplibre from "./map_with_elevation_maplibre.svelte";
|
||||||
import TrailTimeline from "./trail_timeline.svelte";
|
import TrailTimeline from "./trail_timeline.svelte";
|
||||||
|
import Chip from "../base/chip.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initTrail: Trail;
|
initTrail: Trail;
|
||||||
@@ -246,7 +247,7 @@
|
|||||||
</h5>
|
</h5>
|
||||||
{/if}
|
{/if}
|
||||||
{#if trail.expand?.author}
|
{#if trail.expand?.author}
|
||||||
<p class="my-3">
|
<p class="mt-2 mb-3">
|
||||||
{$_("by")}
|
{$_("by")}
|
||||||
<img
|
<img
|
||||||
class="rounded-full w-8 aspect-square mx-1 inline"
|
class="rounded-full w-8 aspect-square mx-1 inline"
|
||||||
@@ -268,7 +269,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-wrap gap-x-8 gap-y-2 mt-4 mr-8">
|
{#if trail.expand?.tags && trail.expand.tags.length > 0}
|
||||||
|
<div class="flex flex-wrap gap-2 text-gray-600">
|
||||||
|
{#each trail.expand.tags as tag}
|
||||||
|
<Chip text={tag.name}></Chip>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-wrap gap-x-8 gap-y-2 mt-2 mr-8">
|
||||||
{#if trail.location}
|
{#if trail.location}
|
||||||
<h3 class="text-lg">
|
<h3 class="text-lg">
|
||||||
<i class="fa fa-location-dot mr-2"></i>
|
<i class="fa fa-location-dot mr-2"></i>
|
||||||
@@ -342,15 +350,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
{#if trail.tags && trail.tags.length > 0}
|
|
||||||
<hr class="border-separator" />
|
|
||||||
<section class="flex p-8 gap-4 text-gray-600">
|
|
||||||
{#each trail.tags as tag}
|
|
||||||
<span class="py-2 px-4 border rounded-full">{tag}</span>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
<hr class="border-separator" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<section class="trail-info-panel-content px-8">
|
<section class="trail-info-panel-content px-8">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
} from "$lib/util/format_util";
|
} from "$lib/util/format_util";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import ShareInfo from "../share_info.svelte";
|
import ShareInfo from "../share_info.svelte";
|
||||||
|
import Chip from "../base/chip.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trail: Trail;
|
trail: Trail;
|
||||||
@@ -88,6 +89,13 @@
|
|||||||
{trail.expand.author.username}
|
{trail.expand.author.username}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if trail.tags?.length}
|
||||||
|
<div class="flex gap-1 mb-3">
|
||||||
|
{#each trail.tags ?? [] as t}
|
||||||
|
<Chip text={t} closable={false} primary={false}></Chip>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex flex-wrap gap-x-8">
|
<div class="flex flex-wrap gap-x-8">
|
||||||
{#if trail.location}
|
{#if trail.location}
|
||||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "Datei {file} ist zu groß (max. {size})",
|
"file-too-big": "Datei {file} ist zu groß (max. {size})",
|
||||||
"filter-categories": "Kategorien filtern",
|
"filter-categories": "Kategorien filtern",
|
||||||
"filter-difficulty": "Schwierigkeit filtern",
|
"filter-difficulty": "Schwierigkeit filtern",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Ziel",
|
"finish": "Ziel",
|
||||||
"focus-map-on": "Karte fokussieren auf",
|
"focus-map-on": "Karte fokussieren auf",
|
||||||
"follow": "Folgen",
|
"follow": "Folgen",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Bearbeiten beenden",
|
"stop-editing": "Bearbeiten beenden",
|
||||||
"summit-book": "Gipfelbuch",
|
"summit-book": "Gipfelbuch",
|
||||||
"table": "Tabelle",
|
"table": "Tabelle",
|
||||||
|
"tags": "",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"tilesets": "Tilesets",
|
"tilesets": "Tilesets",
|
||||||
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"filter-tags": "Filter tags",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Focus map on",
|
"focus-map-on": "Focus map on",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "Summit Book",
|
"summit-book": "Summit Book",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
|
"tags": "Tags",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"tilesets": "Custom tilesets",
|
"tilesets": "Custom tilesets",
|
||||||
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "Archivo {file} es demasiado grande (max. {size})",
|
"file-too-big": "Archivo {file} es demasiado grande (max. {size})",
|
||||||
"filter-categories": "Filtrar categorías",
|
"filter-categories": "Filtrar categorías",
|
||||||
"filter-difficulty": "Filtrar dificultad",
|
"filter-difficulty": "Filtrar dificultad",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Centrar mapa sobre",
|
"focus-map-on": "Centrar mapa sobre",
|
||||||
"follow": "Seguir",
|
"follow": "Seguir",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "Libro de ascensos",
|
"summit-book": "Libro de ascensos",
|
||||||
"table": "Tabla",
|
"table": "Tabla",
|
||||||
|
"tags": "",
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
"tilesets": "Ficha personalizada",
|
"tilesets": "Ficha personalizada",
|
||||||
"trail": "{n, plural, one {}=1 {Ruta} other {Rutas}}",
|
"trail": "{n, plural, one {}=1 {Ruta} other {Rutas}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "Le fichier {file} est trop volumineux (max. {size})",
|
"file-too-big": "Le fichier {file} est trop volumineux (max. {size})",
|
||||||
"filter-categories": "Filtrer les catégories",
|
"filter-categories": "Filtrer les catégories",
|
||||||
"filter-difficulty": "Filtrer la difficulté",
|
"filter-difficulty": "Filtrer la difficulté",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Centrer la carte sur",
|
"focus-map-on": "Centrer la carte sur",
|
||||||
"follow": "Suivre",
|
"follow": "Suivre",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Arrêter la modification",
|
"stop-editing": "Arrêter la modification",
|
||||||
"summit-book": "Liste des ascensions",
|
"summit-book": "Liste des ascensions",
|
||||||
"table": "Tableau",
|
"table": "Tableau",
|
||||||
|
"tags": "",
|
||||||
"text": "Texte",
|
"text": "Texte",
|
||||||
"tilesets": "Tuiles personnalisés",
|
"tilesets": "Tuiles personnalisés",
|
||||||
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
|
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Focus map on",
|
"focus-map-on": "Focus map on",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "Csúcspont könyv",
|
"summit-book": "Csúcspont könyv",
|
||||||
"table": "Táblázat",
|
"table": "Táblázat",
|
||||||
|
"tags": "",
|
||||||
"text": "Szöveg",
|
"text": "Szöveg",
|
||||||
"tilesets": "Custom tilesets",
|
"tilesets": "Custom tilesets",
|
||||||
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
|
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "File {file} è troppo grande (max. {size})",
|
"file-too-big": "File {file} è troppo grande (max. {size})",
|
||||||
"filter-categories": "Filtrare categorie",
|
"filter-categories": "Filtrare categorie",
|
||||||
"filter-difficulty": "Filtrare difficoltà",
|
"filter-difficulty": "Filtrare difficoltà",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Focus sulla mappa",
|
"focus-map-on": "Focus sulla mappa",
|
||||||
"follow": "Seguire",
|
"follow": "Seguire",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "Libro di vetta",
|
"summit-book": "Libro di vetta",
|
||||||
"table": "Tavolo",
|
"table": "Tavolo",
|
||||||
|
"tags": "",
|
||||||
"text": "Testo",
|
"text": "Testo",
|
||||||
"tilesets": "Riquadri personalizzati",
|
"tilesets": "Riquadri personalizzati",
|
||||||
"trail": "{n, plural, =1 {Percorso} other {Percorsi}}",
|
"trail": "{n, plural, =1 {Percorso} other {Percorsi}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filter categories",
|
"filter-categories": "Filter categories",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Focus map on",
|
"focus-map-on": "Focus map on",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "Bergtopboek",
|
"summit-book": "Bergtopboek",
|
||||||
"table": "Tabel",
|
"table": "Tabel",
|
||||||
|
"tags": "",
|
||||||
"text": "Tekst",
|
"text": "Tekst",
|
||||||
"tilesets": "Custom tilesets",
|
"tilesets": "Custom tilesets",
|
||||||
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",
|
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "Plik {file} jest za duży (maks. {size})",
|
"file-too-big": "Plik {file} jest za duży (maks. {size})",
|
||||||
"filter-categories": "Filtruj kategorie",
|
"filter-categories": "Filtruj kategorie",
|
||||||
"filter-difficulty": "Filtruj poziom trudności",
|
"filter-difficulty": "Filtruj poziom trudności",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Skoncentruj mapę na",
|
"focus-map-on": "Skoncentruj mapę na",
|
||||||
"follow": "Obserwuj",
|
"follow": "Obserwuj",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Zakończ edycję",
|
"stop-editing": "Zakończ edycję",
|
||||||
"summit-book": "Logbook",
|
"summit-book": "Logbook",
|
||||||
"table": "Tabela",
|
"table": "Tabela",
|
||||||
|
"tags": "",
|
||||||
"text": "Tekst",
|
"text": "Tekst",
|
||||||
"tilesets": "Niestandardowe zestawy płytek",
|
"tilesets": "Niestandardowe zestawy płytek",
|
||||||
"trail": "{n, plural, one {Szlak} few {Szlaki} many {Szlaków}=1 {Szlak} other {Szlaki}}",
|
"trail": "{n, plural, one {Szlak} few {Szlaki} many {Szlaków}=1 {Szlak} other {Szlaki}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "Filtrar categorias",
|
"filter-categories": "Filtrar categorias",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "Centrar mapa em",
|
"focus-map-on": "Centrar mapa em",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "Livro da cimeira",
|
"summit-book": "Livro da cimeira",
|
||||||
"table": "Tabela",
|
"table": "Tabela",
|
||||||
|
"tags": "",
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
"tilesets": "Camada de renderização personalizada",
|
"tilesets": "Camada de renderização personalizada",
|
||||||
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
|
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"file-too-big": "File {file} is too big (max. {size})",
|
"file-too-big": "File {file} is too big (max. {size})",
|
||||||
"filter-categories": "筛选分类",
|
"filter-categories": "筛选分类",
|
||||||
"filter-difficulty": "Filter difficulty",
|
"filter-difficulty": "Filter difficulty",
|
||||||
|
"filter-tags": "",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"focus-map-on": "地图聚焦于",
|
"focus-map-on": "地图聚焦于",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
"stop-editing": "Stop editing",
|
"stop-editing": "Stop editing",
|
||||||
"summit-book": "详细日程",
|
"summit-book": "详细日程",
|
||||||
"table": "表格",
|
"table": "表格",
|
||||||
|
"tags": "",
|
||||||
"text": "文本",
|
"text": "文本",
|
||||||
"tilesets": "自定义地图图层",
|
"tilesets": "自定义地图图层",
|
||||||
"trail": "{n, plural, =1 {路线} other {路线}}",
|
"trail": "{n, plural, =1 {路线} other {路线}}",
|
||||||
|
|||||||
15
web/src/lib/models/api/tag_schema.ts
Normal file
15
web/src/lib/models/api/tag_schema.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { z, ZodType } from "zod";
|
||||||
|
import type { SummitLog } from "../summit_log";
|
||||||
|
import type { Tag } from "../tag";
|
||||||
|
|
||||||
|
|
||||||
|
const TagCreateSchema = z.object({
|
||||||
|
id: z.string().length(15).optional(),
|
||||||
|
name: z.string()
|
||||||
|
}) satisfies ZodType<Tag>
|
||||||
|
|
||||||
|
const TagUpdateSchema = z.object({
|
||||||
|
name: z.string().optional()
|
||||||
|
}) satisfies ZodType<Partial<Tag>>
|
||||||
|
|
||||||
|
export { TagCreateSchema, TagUpdateSchema };
|
||||||
@@ -21,6 +21,7 @@ const TrailCreateSchema = z.object({
|
|||||||
waypoints: z.array(z.string()).default([]),
|
waypoints: z.array(z.string()).default([]),
|
||||||
summit_logs: z.array(z.string()).default([]),
|
summit_logs: z.array(z.string()).default([]),
|
||||||
category: z.string().length(15).optional().or(z.literal('')),
|
category: z.string().length(15).optional().or(z.literal('')),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
gpx: z.string().optional(),
|
gpx: z.string().optional(),
|
||||||
author: z.string().length(15),
|
author: z.string().length(15),
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ const TrailUpdateSchema = z.object({
|
|||||||
waypoints: z.array(z.string()).optional(),
|
waypoints: z.array(z.string()).optional(),
|
||||||
summit_logs: z.array(z.string()).optional(),
|
summit_logs: z.array(z.string()).optional(),
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
gpx: z.string().optional(),
|
gpx: z.string().optional(),
|
||||||
}) satisfies ZodType<Partial<Trail>>
|
}) satisfies ZodType<Partial<Trail>>
|
||||||
|
|
||||||
|
|||||||
21
web/src/lib/models/tag.ts
Normal file
21
web/src/lib/models/tag.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Trail } from "./trail";
|
||||||
|
|
||||||
|
export class Tag {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TrailTag {
|
||||||
|
id?: string;
|
||||||
|
tag: Tag;
|
||||||
|
trail: Trail;
|
||||||
|
|
||||||
|
constructor(tag: Tag, trail: Trail) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.trail = trail;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Category } from "./category";
|
import type { Category } from "./category";
|
||||||
import type { Comment } from "./comment";
|
import type { Comment } from "./comment";
|
||||||
import type { SummitLog } from "./summit_log";
|
import type { SummitLog } from "./summit_log";
|
||||||
|
import type { Tag } from "./tag";
|
||||||
import type { TrailShare } from "./trail_share";
|
import type { TrailShare } from "./trail_share";
|
||||||
import type { UserAnonymous } from "./user";
|
import type { UserAnonymous } from "./user";
|
||||||
import type { Waypoint } from "./waypoint";
|
import type { Waypoint } from "./waypoint";
|
||||||
@@ -23,9 +24,11 @@ class Trail {
|
|||||||
gpx?: string;
|
gpx?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
tags: string[];
|
||||||
waypoints: string[];
|
waypoints: string[];
|
||||||
summit_logs: string[];
|
summit_logs: string[];
|
||||||
expand?: {
|
expand?: {
|
||||||
|
tags?: Tag[]
|
||||||
category?: Category;
|
category?: Category;
|
||||||
waypoints?: Waypoint[]
|
waypoints?: Waypoint[]
|
||||||
summit_logs?: SummitLog[]
|
summit_logs?: SummitLog[]
|
||||||
@@ -34,7 +37,6 @@ class Trail {
|
|||||||
gpx_data?: string
|
gpx_data?: string
|
||||||
trail_share_via_trail?: TrailShare[]
|
trail_share_via_trail?: TrailShare[]
|
||||||
}
|
}
|
||||||
tags?: string[];
|
|
||||||
description?: string;
|
description?: string;
|
||||||
author: string;
|
author: string;
|
||||||
|
|
||||||
@@ -81,6 +83,7 @@ class Trail {
|
|||||||
this.photos = params?.photos ?? [];
|
this.photos = params?.photos ?? [];
|
||||||
this.waypoints = [];
|
this.waypoints = [];
|
||||||
this.summit_logs = [];
|
this.summit_logs = [];
|
||||||
|
this.tags = []
|
||||||
this.gpx = params?.gpx;
|
this.gpx = params?.gpx;
|
||||||
this.expand = {
|
this.expand = {
|
||||||
category: params?.category,
|
category: params?.category,
|
||||||
@@ -89,7 +92,6 @@ class Trail {
|
|||||||
comments_via_trail: params?.comments ?? [],
|
comments_via_trail: params?.comments ?? [],
|
||||||
trail_share_via_trail: params?.shares ?? []
|
trail_share_via_trail: params?.shares ?? []
|
||||||
}
|
}
|
||||||
this.tags = params?.tags ?? []
|
|
||||||
this.description = params?.description ?? "";
|
this.description = params?.description ?? "";
|
||||||
this.created = params?.created;
|
this.created = params?.created;
|
||||||
this.author = "000000000000000"
|
this.author = "000000000000000"
|
||||||
@@ -99,6 +101,7 @@ class Trail {
|
|||||||
interface TrailFilter {
|
interface TrailFilter {
|
||||||
q: string,
|
q: string,
|
||||||
category: string[],
|
category: string[],
|
||||||
|
tags: string[],
|
||||||
difficulty: ("easy" | "moderate" | "difficult")[]
|
difficulty: ("easy" | "moderate" | "difficult")[]
|
||||||
author?: string;
|
author?: string;
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
@@ -163,6 +166,7 @@ interface TrailSearchResult {
|
|||||||
public: boolean;
|
public: boolean;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
shares?: string[];
|
shares?: string[];
|
||||||
|
tags?: string[]
|
||||||
gpx: string;
|
gpx: string;
|
||||||
_geo: {
|
_geo: {
|
||||||
lat: number,
|
lat: number,
|
||||||
|
|||||||
42
web/src/lib/stores/tag_store.ts
Normal file
42
web/src/lib/stores/tag_store.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { type Tag } from "$lib/models/tag";
|
||||||
|
import { APIError } from "$lib/util/api_util";
|
||||||
|
import type { ListResult } from "pocketbase";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
let tags: Writable<Tag[]> = writable([]);
|
||||||
|
|
||||||
|
|
||||||
|
export async function tags_index(name: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
|
const r = await f('/api/v1/tag?' + new URLSearchParams({
|
||||||
|
filter: `name~'${name}'`,
|
||||||
|
}), {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ListResult<Tag> = await r.json();
|
||||||
|
|
||||||
|
tags.set(response.items);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tags_create(tag: Tag) {
|
||||||
|
let r = await fetch('/api/v1/tag', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(tag),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await r.json();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import { writable, type Writable } from "svelte/store";
|
|||||||
import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_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 { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store";
|
||||||
import { APIError } from "$lib/util/api_util";
|
import { APIError } from "$lib/util/api_util";
|
||||||
|
import { tags_create } from "./tag_store";
|
||||||
|
import type { Tag } from "$lib/models/tag";
|
||||||
|
|
||||||
let trails: Trail[] = []
|
let trails: Trail[] = []
|
||||||
export const trail: Writable<Trail> = writable(new Trail(""));
|
export const trail: Writable<Trail> = writable(new Trail(""));
|
||||||
@@ -20,7 +22,7 @@ export const editTrail: Writable<Trail> = writable(new Trail(""));
|
|||||||
export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
const r = await f('/api/v1/trail?' + new URLSearchParams({
|
const r = await f('/api/v1/trail?' + new URLSearchParams({
|
||||||
"perPage": perPage.toString(),
|
"perPage": perPage.toString(),
|
||||||
expand: "category,waypoints,summit_logs",
|
expand: "category,waypoints,summit_logs,tags",
|
||||||
sort: random ? "@random" : "",
|
sort: random ? "@random" : "",
|
||||||
}), {
|
}), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -127,7 +129,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
|
|||||||
|
|
||||||
export async function trails_show(id: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
export async function trails_show(id: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
|
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
|
||||||
expand: "category,waypoints,summit_logs,trail_share_via_trail",
|
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||||
}), {
|
}), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
})
|
||||||
@@ -181,11 +183,19 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
|||||||
const model = await summit_logs_create(summitLog, f);
|
const model = await summit_logs_create(summitLog, f);
|
||||||
trail.summit_logs.push(model.id!);
|
trail.summit_logs.push(model.id!);
|
||||||
}
|
}
|
||||||
|
for (const tag of trail.expand?.tags ?? []) {
|
||||||
|
if (!tag.id) {
|
||||||
|
const model = await tags_create(tag)
|
||||||
|
trail.tags.push(model.id!)
|
||||||
|
} else {
|
||||||
|
trail.tags.push(tag.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trail.author = pb.authStore.record!.id
|
trail.author = pb.authStore.record!.id
|
||||||
|
|
||||||
let r = await f(`/api/v1/trail?` + new URLSearchParams({
|
let r = await f(`/api/v1/trail?` + new URLSearchParams({
|
||||||
expand: "category,waypoints,summit_logs,trail_share_via_trail",
|
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||||
}), {
|
}), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ ...trail }),
|
body: JSON.stringify({ ...trail }),
|
||||||
@@ -263,8 +273,23 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
|||||||
const success = await summit_logs_delete(deletedSummitLog);
|
const success = await summit_logs_delete(deletedSummitLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tagUpdates = compareObjectArrays<Tag>(oldTrail.expand?.tags ?? [], newTrail.expand?.tags ?? []);
|
||||||
|
|
||||||
|
for (const tag of tagUpdates.added) {
|
||||||
|
if (!tag.id) {
|
||||||
|
const model = await tags_create(tag)
|
||||||
|
newTrail.tags.push(model.id!)
|
||||||
|
} else {
|
||||||
|
newTrail.tags.push(tag.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of tagUpdates.deleted) {
|
||||||
|
newTrail.tags = newTrail.tags.filter(t => t != tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
let r = await fetch(`/api/v1/trail/${newTrail.id}?` + new URLSearchParams({
|
let r = await fetch(`/api/v1/trail/${newTrail.id}?` + new URLSearchParams({
|
||||||
expand: "category,waypoints,summit_logs,trail_share_via_trail",
|
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||||
}), {
|
}), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ...newTrail, expand: undefined }),
|
body: JSON.stringify({ ...newTrail, expand: undefined }),
|
||||||
@@ -430,6 +455,7 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: b
|
|||||||
public: h.public,
|
public: h.public,
|
||||||
summit_logs: [],
|
summit_logs: [],
|
||||||
waypoints: [],
|
waypoints: [],
|
||||||
|
tags: h.tags ?? [],
|
||||||
category: h.category,
|
category: h.category,
|
||||||
created: new Date(h.created * 1000).toISOString(),
|
created: new Date(h.created * 1000).toISOString(),
|
||||||
date: new Date(h.date * 1000).toISOString(),
|
date: new Date(h.date * 1000).toISOString(),
|
||||||
@@ -529,6 +555,11 @@ function buildFilterText(filter: TrailFilter, includeGeo: boolean): string {
|
|||||||
if (filter.category.length > 0) {
|
if (filter.category.length > 0) {
|
||||||
filterText += ` AND category IN [${filter.category.join(",")}]`;
|
filterText += ` AND category IN [${filter.category.join(",")}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter.tags.length > 0) {
|
||||||
|
filterText += ` AND (${filter.tags.map(t => `tags = '${t}'`).join(" OR ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
if (filter.completed !== undefined) {
|
if (filter.completed !== undefined) {
|
||||||
filterText += ` AND completed = ${filter.completed}`;
|
filterText += ` AND completed = ${filter.completed}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export enum Collection {
|
|||||||
summit_logs = "summit_logs",
|
summit_logs = "summit_logs",
|
||||||
trail_share = "trail_share",
|
trail_share = "trail_share",
|
||||||
trails = "trails",
|
trails = "trails",
|
||||||
|
tags = "tags",
|
||||||
waypoints = "waypoints",
|
waypoints = "waypoints",
|
||||||
activities = "activities",
|
activities = "activities",
|
||||||
follow_counts = "follow_counts",
|
follow_counts = "follow_counts",
|
||||||
|
|||||||
23
web/src/routes/api/v1/tag/+server.ts
Normal file
23
web/src/routes/api/v1/tag/+server.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { TagCreateSchema } from '$lib/models/api/tag_schema';
|
||||||
|
import type { Tag } from '$lib/models/tag';
|
||||||
|
import { Collection, create, handleError, list } from '$lib/util/api_util';
|
||||||
|
import { json, type RequestEvent } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function GET(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await list<Tag>(event, Collection.tags);
|
||||||
|
|
||||||
|
return json(r)
|
||||||
|
} catch (e: any) {
|
||||||
|
throw handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await create<Tag>(event, TagCreateSchema, Collection.tags)
|
||||||
|
return json(r);
|
||||||
|
} catch (e) {
|
||||||
|
throw handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
web/src/routes/api/v1/tag/[id]/+server.ts
Normal file
31
web/src/routes/api/v1/tag/[id]/+server.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { TagUpdateSchema } from "$lib/models/api/tag_schema";
|
||||||
|
import type { Tag } from "$lib/models/tag";
|
||||||
|
import { Collection, handleError, remove, show, update } from "$lib/util/api_util";
|
||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function GET(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await show<Tag>(event, Collection.tags)
|
||||||
|
return json(r)
|
||||||
|
} catch (e: any) {
|
||||||
|
throw handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await update<Tag>(event, TagUpdateSchema, Collection.tags)
|
||||||
|
return json(r);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await remove(event, Collection.tags)
|
||||||
|
return json(r);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
|||||||
const filter: TrailFilter = {
|
const filter: TrailFilter = {
|
||||||
q: "",
|
q: "",
|
||||||
category: [],
|
category: [],
|
||||||
|
tags: [],
|
||||||
difficulty: ["easy", "moderate", "difficult"],
|
difficulty: ["easy", "moderate", "difficult"],
|
||||||
author: "",
|
author: "",
|
||||||
public: true,
|
public: true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const load: Load = async ({ params, fetch }) => {
|
|||||||
const filter: TrailFilter = {
|
const filter: TrailFilter = {
|
||||||
q: "",
|
q: "",
|
||||||
category: [],
|
category: [],
|
||||||
|
tags: [],
|
||||||
difficulty: ["easy", "moderate", "difficult"],
|
difficulty: ["easy", "moderate", "difficult"],
|
||||||
author: params.id,
|
author: params.id,
|
||||||
public: true,
|
public: true,
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
import { backInOut } from "svelte/easing";
|
import { backInOut } from "svelte/easing";
|
||||||
import { scale } from "svelte/transition";
|
import { scale } from "svelte/transition";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import Combobox, {
|
||||||
|
type ComboboxItem,
|
||||||
|
} from "$lib/components/base/combobox.svelte";
|
||||||
|
import { TagCreateSchema } from "$lib/models/api/tag_schema.js";
|
||||||
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
|
import { Tag } from "$lib/models/tag.js";
|
||||||
|
import { tags_index } from "$lib/stores/tag_store.js";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -117,6 +124,7 @@
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
tags: z.array(TagCreateSchema).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
@@ -132,6 +140,8 @@
|
|||||||
|
|
||||||
let savedAtLeastOnce = $state(false);
|
let savedAtLeastOnce = $state(false);
|
||||||
|
|
||||||
|
let tagItems: ComboboxItem[] = $state([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
errors,
|
errors,
|
||||||
@@ -202,7 +212,7 @@
|
|||||||
);
|
);
|
||||||
setFields(updatedTrail);
|
setFields(updatedTrail);
|
||||||
}
|
}
|
||||||
photoFiles = []
|
photoFiles = [];
|
||||||
|
|
||||||
savedAtLeastOnce = true;
|
savedAtLeastOnce = true;
|
||||||
show_toast({
|
show_toast({
|
||||||
@@ -406,8 +416,8 @@
|
|||||||
const wp = $formData.expand!.waypoints?.splice(index, 1);
|
const wp = $formData.expand!.waypoints?.splice(index, 1);
|
||||||
$formData.waypoints.splice(index, 1);
|
$formData.waypoints.splice(index, 1);
|
||||||
|
|
||||||
if(!$formData.expand!.waypoints?.length) {
|
if (!$formData.expand!.waypoints?.length) {
|
||||||
$formData.expand!.waypoints = []
|
$formData.expand!.waypoints = [];
|
||||||
}
|
}
|
||||||
$formData.expand!.waypoints = $formData.expand!.waypoints;
|
$formData.expand!.waypoints = $formData.expand!.waypoints;
|
||||||
|
|
||||||
@@ -546,7 +556,11 @@
|
|||||||
|
|
||||||
async function handleMapClick(e: M.MapMouseEvent) {
|
async function handleMapClick(e: M.MapMouseEvent) {
|
||||||
if (!drawingActive) {
|
if (!drawingActive) {
|
||||||
if ((e.originalEvent.target as HTMLElement).tagName.toLowerCase() !== "canvas") {
|
if (
|
||||||
|
(
|
||||||
|
e.originalEvent.target as HTMLElement
|
||||||
|
).tagName.toLowerCase() !== "canvas"
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mapPopup?.remove();
|
mapPopup?.remove();
|
||||||
@@ -853,6 +867,26 @@
|
|||||||
untrack(() => updateTrailOnMap());
|
untrack(() => updateTrailOnMap());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getTrailTags() {
|
||||||
|
return (
|
||||||
|
$formData.expand?.tags?.map((t) => ({
|
||||||
|
text: t.name,
|
||||||
|
value: t,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTrailTags(items: ComboboxItem[]) {
|
||||||
|
$formData.expand!.tags = items.map((i) =>
|
||||||
|
i.value ? i.value : new Tag(i.text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchTags(q: string) {
|
||||||
|
const result = await tags_index(q);
|
||||||
|
tagItems = result.items.map((t) => ({ text: t.name, value: t }));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -1009,6 +1043,14 @@
|
|||||||
<Datepicker label={$_("date")} bind:value={$formData.date}></Datepicker>
|
<Datepicker label={$_("date")} bind:value={$formData.date}></Datepicker>
|
||||||
<Textarea name="description" label={$_("describe-your-trail")}
|
<Textarea name="description" label={$_("describe-your-trail")}
|
||||||
></Textarea>
|
></Textarea>
|
||||||
|
<Combobox
|
||||||
|
bind:value={getTrailTags, setTrailTags}
|
||||||
|
onupdate={searchTags}
|
||||||
|
items={tagItems}
|
||||||
|
label={$_("tags")}
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
></Combobox>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-4">
|
||||||
<Select
|
<Select
|
||||||
name="difficulty"
|
name="difficulty"
|
||||||
@@ -1036,7 +1078,10 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $formData.expand?.waypoints ?? [] as waypoint, i}
|
{#each $formData.expand?.waypoints ?? [] as waypoint, i}
|
||||||
<li onmouseenter={() => openMarkerPopup(waypoint)} onmouseleave={() => openMarkerPopup(waypoint)}>
|
<li
|
||||||
|
onmouseenter={() => openMarkerPopup(waypoint)}
|
||||||
|
onmouseleave={() => openMarkerPopup(waypoint)}
|
||||||
|
>
|
||||||
<WaypointCard
|
<WaypointCard
|
||||||
{waypoint}
|
{waypoint}
|
||||||
mode="edit"
|
mode="edit"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
|
|||||||
const filter: TrailFilter = {
|
const filter: TrailFilter = {
|
||||||
q: "",
|
q: "",
|
||||||
category: [],
|
category: [],
|
||||||
|
tags: [],
|
||||||
difficulty: ["easy", "moderate", "difficult"],
|
difficulty: ["easy", "moderate", "difficult"],
|
||||||
author: "",
|
author: "",
|
||||||
public: true,
|
public: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user