From c38d41546034fd3840c60d5c683e77429cfc0578 Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Fri, 13 Sep 2024 20:06:46 +0200 Subject: [PATCH] adds trail share --- .../1726231220_created_list_share.go | 96 ++++++ .../1726232380_updated_list_share.go | 50 +++ db/migrations/1726232603_updated_lists.go | 46 +++ .../1726245829_updated_trails_filter.go | 300 ++++++++++++++++++ web/src/lib/components/confirm_modal.svelte | 3 +- web/src/lib/components/list/list_card.svelte | 44 ++- .../components/list/list_share_modal.svelte | 222 +++++++++++++ ...il_share_info.svelte => share_info.svelte} | 26 +- .../lib/components/trail/trail_card.svelte | 4 +- .../components/trail/trail_info_panel.svelte | 4 +- .../components/trail/trail_list_item.svelte | 4 +- .../components/trail/trail_share_modal.svelte | 2 +- web/src/lib/i18n/locales/de.json | 7 + web/src/lib/i18n/locales/en.json | 7 + web/src/lib/i18n/locales/fr.json | 7 + web/src/lib/i18n/locales/hu.json | 7 + web/src/lib/i18n/locales/it.json | 7 + web/src/lib/i18n/locales/nl.json | 7 + web/src/lib/i18n/locales/pl.json | 7 + web/src/lib/i18n/locales/pt.json | 7 + web/src/lib/i18n/locales/zh.json | 7 + web/src/lib/models/list.ts | 3 + web/src/lib/models/list_share.ts | 17 + web/src/lib/stores/list_share_store.ts | 57 ++++ web/src/lib/stores/trail_share_store.ts | 8 +- .../libs/leaflet-gpxgroup.js | 15 +- web/src/routes/api/v1/list-share/+server.ts | 36 +++ .../routes/api/v1/list-share/[id]/+server.ts | 33 ++ web/src/routes/api/v1/list/+server.ts | 2 +- web/src/routes/api/v1/list/[id]/+server.ts | 2 +- web/src/routes/lists/+page.svelte | 62 ++-- web/src/routes/lists/edit/[id]/+page.svelte | 140 ++++++-- web/src/routes/lists/edit/[id]/+page.ts | 7 +- web/src/routes/map/trail/[id]/+page.svelte | 2 +- 34 files changed, 1144 insertions(+), 104 deletions(-) create mode 100644 db/migrations/1726231220_created_list_share.go create mode 100644 db/migrations/1726232380_updated_list_share.go create mode 100644 db/migrations/1726232603_updated_lists.go create mode 100644 db/migrations/1726245829_updated_trails_filter.go create mode 100644 web/src/lib/components/list/list_share_modal.svelte rename web/src/lib/components/{trail/trail_share_info.svelte => share_info.svelte} (87%) create mode 100644 web/src/lib/models/list_share.ts create mode 100644 web/src/lib/stores/list_share_store.ts create mode 100644 web/src/routes/api/v1/list-share/+server.ts create mode 100644 web/src/routes/api/v1/list-share/[id]/+server.ts diff --git a/db/migrations/1726231220_created_list_share.go b/db/migrations/1726231220_created_list_share.go new file mode 100644 index 00000000..3330ab83 --- /dev/null +++ b/db/migrations/1726231220_created_list_share.go @@ -0,0 +1,96 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" +) + +func init() { + m.Register(func(db dbx.Builder) error { + jsonData := `{ + "id": "1kot7t9na3hi0gl", + "created": "2024-09-13 12:40:20.308Z", + "updated": "2024-09-13 12:40:20.308Z", + "name": "list_share", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "luqrtipy", + "name": "list", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "r6gu2ajyidy1x69", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "mix12kkh", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "_pb_users_auth_", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "n9rjdx5g", + "name": "permission", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "view", + "edit" + ] + } + } + ], + "indexes": [], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }` + + collection := &models.Collection{} + if err := json.Unmarshal([]byte(jsonData), &collection); err != nil { + return err + } + + return daos.New(db).SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("1kot7t9na3hi0gl") + if err != nil { + return err + } + + return dao.DeleteCollection(collection) + }) +} diff --git a/db/migrations/1726232380_updated_list_share.go b/db/migrations/1726232380_updated_list_share.go new file mode 100644 index 00000000..5b70b9d0 --- /dev/null +++ b/db/migrations/1726232380_updated_list_share.go @@ -0,0 +1,50 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("1kot7t9na3hi0gl") + if err != nil { + return err + } + + collection.ListRule = types.Pointer("list.author = @request.auth.id || user = @request.auth.id") + + collection.ViewRule = types.Pointer("list.author = @request.auth.id || user = @request.auth.id") + + collection.CreateRule = types.Pointer("list.author = @request.auth.id") + + collection.UpdateRule = types.Pointer("list.author = @request.auth.id") + + collection.DeleteRule = types.Pointer("list.author = @request.auth.id") + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("1kot7t9na3hi0gl") + if err != nil { + return err + } + + collection.ListRule = nil + + collection.ViewRule = nil + + collection.CreateRule = nil + + collection.UpdateRule = nil + + collection.DeleteRule = nil + + return dao.SaveCollection(collection) + }) +} diff --git a/db/migrations/1726232603_updated_lists.go b/db/migrations/1726232603_updated_lists.go new file mode 100644 index 00000000..c5433c4b --- /dev/null +++ b/db/migrations/1726232603_updated_lists.go @@ -0,0 +1,46 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69") + if err != nil { + return err + } + + collection.ListRule = types.Pointer("author = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.user ?= @request.auth.id)") + + collection.ViewRule = types.Pointer("author = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.user ?= @request.auth.id)") + + collection.CreateRule = types.Pointer("@request.auth.id != \"\" && (@request.data.author = @request.auth.id)") + + collection.UpdateRule = types.Pointer("author = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.list = id && list_share_via_list.user ?= @request.auth.id && list_share_via_list.permission = \"edit\")") + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69") + if err != nil { + return err + } + + collection.ListRule = types.Pointer("author = @request.auth.id ") + + collection.ViewRule = types.Pointer("author = @request.auth.id ") + + collection.CreateRule = types.Pointer("author = @request.auth.id ") + + collection.UpdateRule = types.Pointer("author = @request.auth.id ") + + return dao.SaveCollection(collection) + }) +} diff --git a/db/migrations/1726245829_updated_trails_filter.go b/db/migrations/1726245829_updated_trails_filter.go new file mode 100644 index 00000000..5850544a --- /dev/null +++ b/db/migrations/1726245829_updated_trails_filter.go @@ -0,0 +1,300 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4wbv9tz5zjdrjh1") + if err != nil { + return err + } + + options := map[string]any{} + if err := json.Unmarshal([]byte(`{ + "query": "SELECT users.id, COALESCE(MAX(trails.distance), 0) AS max_distance, COALESCE(MAX(trails.elevation_gain), 0) AS max_elevation_gain, COALESCE(MAX(trails.duration), 0) AS max_duration, COALESCE(MIN(trails.distance), 0) AS min_distance, COALESCE(MIN(trails.elevation_gain), 0) AS min_elevation_gain, COALESCE(MIN(trails.duration), 0) AS min_duration FROM users LEFT JOIN trails ON users.id = trails.author OR trails.public = 1 OR EXISTS (\n SELECT 1 \n FROM trail_share \n WHERE trail_share.trail = trails.id \n AND trail_share.user = users.id\n ) GROUP BY users.id;" + }`), &options); err != nil { + return err + } + collection.SetOptions(options) + + // remove + collection.Schema.RemoveField("6tuhuumw") + + // remove + collection.Schema.RemoveField("xarhom23") + + // remove + collection.Schema.RemoveField("kl9wsxwf") + + // remove + collection.Schema.RemoveField("ipd1ohgc") + + // remove + collection.Schema.RemoveField("o4s7acfv") + + // remove + collection.Schema.RemoveField("d9lfg9vd") + + // add + new_max_distance := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "ixfa8u8m", + "name": "max_distance", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), new_max_distance); err != nil { + return err + } + collection.Schema.AddField(new_max_distance) + + // add + new_max_elevation_gain := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "bhn0jj40", + "name": "max_elevation_gain", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), new_max_elevation_gain); err != nil { + return err + } + collection.Schema.AddField(new_max_elevation_gain) + + // add + new_max_duration := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "3kujiwzh", + "name": "max_duration", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), new_max_duration); err != nil { + return err + } + collection.Schema.AddField(new_max_duration) + + // add + new_min_distance := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "yf3zwzn4", + "name": "min_distance", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), new_min_distance); err != nil { + return err + } + collection.Schema.AddField(new_min_distance) + + // add + new_min_elevation_gain := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "skvtkith", + "name": "min_elevation_gain", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), new_min_elevation_gain); err != nil { + return err + } + collection.Schema.AddField(new_min_elevation_gain) + + // add + new_min_duration := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "gydvo1ug", + "name": "min_duration", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), new_min_duration); err != nil { + return err + } + collection.Schema.AddField(new_min_duration) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4wbv9tz5zjdrjh1") + if err != nil { + return err + } + + options := map[string]any{} + if err := json.Unmarshal([]byte(`{ + "query": "SELECT users.id, COALESCE(MAX(trails.distance), 0) AS max_distance, COALESCE(MAX(trails.elevation_gain), 0) AS max_elevation_gain, COALESCE(MAX(trails.duration), 0) AS max_duration, COALESCE(MIN(trails.distance), 0) AS min_distance, COALESCE(MIN(trails.elevation_gain), 0) AS min_elevation_gain, COALESCE(MIN(trails.duration), 0) AS min_duration FROM users LEFT JOIN trails ON users.id = trails.author OR trails.public = 1 OR EXISTS (\n SELECT 1 \n FROM trail_share \n WHERE trail_share.trail = trails.id \n AND trail_share.user = users.id\n );" + }`), &options); err != nil { + return err + } + collection.SetOptions(options) + + // add + del_max_distance := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "6tuhuumw", + "name": "max_distance", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), del_max_distance); err != nil { + return err + } + collection.Schema.AddField(del_max_distance) + + // add + del_max_elevation_gain := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "xarhom23", + "name": "max_elevation_gain", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), del_max_elevation_gain); err != nil { + return err + } + collection.Schema.AddField(del_max_elevation_gain) + + // add + del_max_duration := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "kl9wsxwf", + "name": "max_duration", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), del_max_duration); err != nil { + return err + } + collection.Schema.AddField(del_max_duration) + + // add + del_min_distance := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "ipd1ohgc", + "name": "min_distance", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), del_min_distance); err != nil { + return err + } + collection.Schema.AddField(del_min_distance) + + // add + del_min_elevation_gain := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "o4s7acfv", + "name": "min_elevation_gain", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), del_min_elevation_gain); err != nil { + return err + } + collection.Schema.AddField(del_min_elevation_gain) + + // add + del_min_duration := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "d9lfg9vd", + "name": "min_duration", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 1 + } + }`), del_min_duration); err != nil { + return err + } + collection.Schema.AddField(del_min_duration) + + // remove + collection.Schema.RemoveField("ixfa8u8m") + + // remove + collection.Schema.RemoveField("bhn0jj40") + + // remove + collection.Schema.RemoveField("3kujiwzh") + + // remove + collection.Schema.RemoveField("yf3zwzn4") + + // remove + collection.Schema.RemoveField("skvtkith") + + // remove + collection.Schema.RemoveField("gydvo1ug") + + return dao.SaveCollection(collection) + }) +} diff --git a/web/src/lib/components/confirm_modal.svelte b/web/src/lib/components/confirm_modal.svelte index e54ee683..14d6616f 100644 --- a/web/src/lib/components/confirm_modal.svelte +++ b/web/src/lib/components/confirm_modal.svelte @@ -8,6 +8,7 @@ export let title: string = "Confirm Deletion"; export let text: string; + export let action: string = "delete"; const dispatch = createEventDispatcher(); @@ -24,7 +25,7 @@ >{$_("cancel")} {$_(action)} diff --git a/web/src/lib/components/list/list_card.svelte b/web/src/lib/components/list/list_card.svelte index 90016736..f35e51f0 100644 --- a/web/src/lib/components/list/list_card.svelte +++ b/web/src/lib/components/list/list_card.svelte @@ -8,14 +8,11 @@ } from "$lib/util/format_util"; import Dropdown, { type DropdownItem } from "../base/dropdown.svelte"; import { _ } from "svelte-i18n"; + import ShareInfo from "../share_info.svelte"; + import { currentUser } from "$lib/stores/user_store"; export let list: List; export let active: boolean = false; - const dropdownItems: DropdownItem[] = [ - { text: $_("edit"), value: "edit" }, - { text: $_("delete"), value: "delete" }, - ]; - $: cumulativeDistance = list.expand?.trails.reduce( (s, b) => s + b.distance!, 0, @@ -30,6 +27,22 @@ (s, b) => s + b.duration!, 0, ); + + $: listIsShared = (list.expand?.list_share_via_list?.length ?? 0) > 0; + + $: allowEdit = + list.author == $currentUser?.id || + list.expand?.list_share_via_list?.some((s) => s.permission == "edit"); + + $: dropdownItems = [ + ...(list.author == $currentUser?.id + ? [{ text: $_("share"), value: "share" }] + : []), + ...(allowEdit ? [{ text: $_("edit"), value: "edit" }] : []), + ...(list.author == $currentUser?.id + ? [{ text: $_("delete"), value: "delete" }] + : []), + ];
{/if}
-
-
+
+
{list.name}
- + {#if listIsShared} + + {/if} + {#if dropdownItems.length} + + {/if}
-

{list.expand?.trails.length ?? 0} +

+ {list.expand?.trails.length ?? 0} {$_("trail", { values: { n: list.expand?.trails.length ?? 0 }, - })}

+ })} +

+ import Modal from "$lib/components/base/modal.svelte"; + import type { List } from "$lib/models/list"; + import { ListShare } from "$lib/models/list_share"; + import { TrailShare } from "$lib/models/trail_share"; + import type { User } from "$lib/models/user"; + import { + list_share_create, + list_share_delete, + list_share_index, + list_share_update, + shares, + } from "$lib/stores/list_share_store"; + import { show_toast } from "$lib/stores/toast_store"; + import { + trail_share_create, + trail_share_delete, + trail_share_index, + } from "$lib/stores/trail_share_store"; + import { users_search } from "$lib/stores/user_store"; + import { getFileURL } from "$lib/util/file_util"; + import { createEventDispatcher } from "svelte"; + import { _ } from "svelte-i18n"; + import Button from "../base/button.svelte"; + import Search, { type SearchItem } from "../base/search.svelte"; + import type { SelectItem } from "../base/select.svelte"; + import Select from "../base/select.svelte"; + import { useFBO } from "@threlte/extras"; + import { lists } from "$lib/stores/list_store"; + + let openModal: (() => void) | undefined = undefined; + export let closeModal: (() => void) | undefined = undefined; + export function openShareModal() { + shares.set([]); + fetchShares(); + if (openModal) { + openModal(); + } + } + + export let list: List; + + const dispatch = createEventDispatcher(); + + let copyButtonText = $_("copy-link"); + + let searchItems: SearchItem[] = []; + + let sharesLoading: boolean = false; + + const permissionSelectItems: SelectItem[] = [ + { text: $_("view"), value: "view" }, + { text: $_("edit"), value: "edit" }, + ]; + + async function updateUsers(q: string) { + try { + const users: User[] = await users_search(q); + searchItems = users.map((u) => ({ + text: u.username!, + value: u, + icon: "user", + })); + } catch (e) { + console.error(e); + show_toast({ + type: "error", + icon: "close", + text: "Error during search", + }); + } + } + + function copyURLToClipboard() { + navigator.clipboard.writeText(window.location.href); + + copyButtonText = $_("link-copied"); + setTimeout(() => (copyButtonText = $_("copy-link")), 3000); + } + + function close() { + searchItems = []; + dispatch("save"); + closeModal!(); + } + + async function shareTrails(userId: string) { + const existingTrailShares = await trail_share_index({ user: userId }); + const trailIds = existingTrailShares.map((s) => s.trail); + for (const trailId of list.trails ?? []) { + if (!trailIds.includes(trailId)) { + const share = new TrailShare(userId, trailId, "view"); + await trail_share_create(share); + } + } + } + + async function shareList(item: SelectItem) { + const share = new ListShare(item.value.id, list.id!, "view"); + await list_share_create(share); + await shareTrails(item.value.id); + fetchShares(); + } + + async function updateSharePermission( + share: ListShare, + permission: "view" | "edit", + ) { + share.permission = permission; + await list_share_update(share); + } + + async function deleteTrailShares(userId: string) { + const existingTrailShares = await trail_share_index({ user: userId }); + for (const trailId of list.trails ?? []) { + const shareToDelete = existingTrailShares.find( + (s) => s.trail == trailId, + ); + if (shareToDelete) { + await trail_share_delete(shareToDelete); + } + } + } + + async function deleteShare(share: ListShare) { + await list_share_delete(share); + // await deleteTrailShares(share.user); + fetchShares(); + } + + async function fetchShares() { + sharesLoading = true; + const fetchedShares = await list_share_index(list.id!); + list.expand = { + trails: list.expand?.trails ?? [], + list_share_via_list: fetchedShares, + }; + sharesLoading = false; + } + + + +

+

+ {$_("list-share-warning")} +

+ updateUsers(e.detail)} + on:click={(e) => shareList(e.detail)} + placeholder={`${$_("username")}`} + items={searchItems} + > + avatar + +

{$_("shared-with")}

+ + {#if $shares.length == 0} +

+ {$_("list-not-shared")} +

+ {:else} + {#each $shares as share} + {#if share.expand} +
+ avatar +

{share.expand.user.username}

+ {$_("can")} +
+ +
+ + +
+ {/if} + {/each} + {/if} +
+
+ + +
diff --git a/web/src/lib/components/trail/trail_share_info.svelte b/web/src/lib/components/share_info.svelte similarity index 87% rename from web/src/lib/components/trail/trail_share_info.svelte rename to web/src/lib/components/share_info.svelte index 98c77c6e..cb789fe3 100644 --- a/web/src/lib/components/trail/trail_share_info.svelte +++ b/web/src/lib/components/share_info.svelte @@ -1,4 +1,5 @@ diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json index 2a547cf0..6afd9b1d 100644 --- a/web/src/lib/i18n/locales/de.json +++ b/web/src/lib/i18n/locales/de.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Kommentar} other {Kommentare}}", "completed": "Abgeschlossen", "completion-status": "Abschlussstatus", + "confirm": "", + "confirm-share": "", "contribute": "Mitwirken", "copy-link": "Link kopieren", "create-new-list": "Neue Liste erstellen", @@ -71,6 +73,7 @@ "error-exporting-trail": "Fehler beim Exportieren des Trails", "error-printing-map": "Fehler beim Drucken der Karte", "error-reading-file": "Fehler beim Lesen der Datei", + "error-saving-list": "", "error-saving-trail": "Fehler bei Speichern der Route", "error-updating-password": "Fehler beim Aktualisieren des Passworts", "est-duration": "Gesch. Dauer", @@ -106,7 +109,10 @@ "license": "Lizenz", "link-copied": "Link kopiert", "list": "{n, plural, =1 {Liste} other {Listen}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "Sharing a list automatically shares all trails contained in it.", + "list-share-warning-update": "", "location": "Standort", "login": "Login", "login-details": "Login Details", @@ -162,6 +168,7 @@ "select-list": "Liste auswählen", "settings": "Einstellungen", "share": "Teilen", + "share-this-list": "", "share-this-trail": "Diese Route teilen", "shared-by": "Geteilt von", "shared-with": "Geteilt mit", diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index 6cb7aa5c..4045a15e 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Comment} other {Comments}}", "completed": "Completed", "completion-status": "Completion Status", + "confirm": "Confirm", + "confirm-share": "Confirm share", "contribute": "Contribute", "copy-link": "Copy Link", "create-new-list": "Create new list", @@ -71,6 +73,7 @@ "error-exporting-trail": "Error exporting trail", "error-printing-map": "Error printing map", "error-reading-file": "Error reading file", + "error-saving-list": "Error saving list", "error-saving-trail": "Error saving trail", "error-updating-password": "Error updating password", "est-duration": "Est. duration", @@ -106,7 +109,10 @@ "license": "License", "link-copied": "Link copied!", "list": "{n, plural, =1 {List} other {Lists}}", + "list-not-shared": "Not shared with anyone", "list-saved-successfully": "List saved successfully", + "list-share-warning": "", + "list-share-warning-update": "Added trails will be shared with everyone that has access to this list.", "location": "Location", "login": "Login", "login-details": "Login details", @@ -162,6 +168,7 @@ "select-list": "Select List", "settings": "Settings", "share": "Share", + "share-this-list": "Share this list", "share-this-trail": "Share this trail", "shared-by": "Shared by", "shared-with": "Shared with", diff --git a/web/src/lib/i18n/locales/fr.json b/web/src/lib/i18n/locales/fr.json index 4cedcf5c..149969ff 100644 --- a/web/src/lib/i18n/locales/fr.json +++ b/web/src/lib/i18n/locales/fr.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Commentaire} other {Commentaires}}", "completed": "Compléter", "completion-status": "L'état d'achèvement", + "confirm": "", + "confirm-share": "", "contribute": "Contribuer", "copy-link": "", "create-new-list": "Créer une nouvelle liste", @@ -71,6 +73,7 @@ "error-exporting-trail": "", "error-printing-map": "Erreur d'impression de la carte", "error-reading-file": "Erreur de lecture du fichier", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "Temps estimé", @@ -106,7 +109,10 @@ "license": "Licence", "link-copied": "", "list": "{n, plural, =1 {Liste} other {Listes}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "Localisation", "login": "Connexion", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "Liste de choix", "settings": "Paramètres", "share": "", + "share-this-list": "", "share-this-trail": "", "shared-by": "", "shared-with": "", diff --git a/web/src/lib/i18n/locales/hu.json b/web/src/lib/i18n/locales/hu.json index 45359d74..2eddc305 100644 --- a/web/src/lib/i18n/locales/hu.json +++ b/web/src/lib/i18n/locales/hu.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Megjegyzés} other {Megjegyzés}}", "completed": "Teljesítve", "completion-status": "Befejezés állapota", + "confirm": "", + "confirm-share": "", "contribute": "Hozzájárulás", "copy-link": "", "create-new-list": "Új lista létrehozása", @@ -71,6 +73,7 @@ "error-exporting-trail": "", "error-printing-map": "", "error-reading-file": "Hiba a fájl olvasása közben", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "Becsült időtartam", @@ -106,7 +109,10 @@ "license": "License", "link-copied": "", "list": "{n, plural, =1 {Lista} other {Listák}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "Helyszín", "login": "Bejelentkezés", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "Lista kiválasztása", "settings": "Beállítások", "share": "", + "share-this-list": "", "share-this-trail": "", "shared-by": "", "shared-with": "", diff --git a/web/src/lib/i18n/locales/it.json b/web/src/lib/i18n/locales/it.json index 3d41523a..b84398b0 100644 --- a/web/src/lib/i18n/locales/it.json +++ b/web/src/lib/i18n/locales/it.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Commento} other {Commenti}}", "completed": "Completato", "completion-status": "Stato di completamento", + "confirm": "", + "confirm-share": "", "contribute": "Contribuisci", "copy-link": "Copia link", "create-new-list": "Crea nuova lista", @@ -71,6 +73,7 @@ "error-exporting-trail": "Errore durante l'esportazione del percorso", "error-printing-map": "Errore durante la stampa della mappa", "error-reading-file": "Errore durante la lettura del file", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "Durata stimata", @@ -106,7 +109,10 @@ "license": "Licenza", "link-copied": "Link copiato", "list": "{n, plural, =1 {Lista} other {Liste}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "Posizione", "login": "Login", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "Seleziona lista", "settings": "Impostazioni", "share": "Condividi", + "share-this-list": "", "share-this-trail": "Condividi questo percorso", "shared-by": "Condiviso da", "shared-with": "Condiviso con", diff --git a/web/src/lib/i18n/locales/nl.json b/web/src/lib/i18n/locales/nl.json index a261d707..f31fb4e1 100644 --- a/web/src/lib/i18n/locales/nl.json +++ b/web/src/lib/i18n/locales/nl.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Opmerking} other {Opmerkingen}}", "completed": "Voltooid", "completion-status": "Voltooiingsstatus", + "confirm": "", + "confirm-share": "", "contribute": "Bijdragen", "copy-link": "", "create-new-list": "Nieuwe lijst", @@ -71,6 +73,7 @@ "error-exporting-trail": "", "error-printing-map": "Fout bij afdrukken van kaart", "error-reading-file": "Het bestand kan niet worden ingelezen", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "Geschatte duur", @@ -106,7 +109,10 @@ "license": "Licentie", "link-copied": "", "list": "{n, plural, =1 {Lijst} other {Lijsten}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "Locatie", "login": "Inloggen", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "Kies een lijst", "settings": "Instellingen", "share": "", + "share-this-list": "", "share-this-trail": "", "shared-by": "", "shared-with": "", diff --git a/web/src/lib/i18n/locales/pl.json b/web/src/lib/i18n/locales/pl.json index ad9cd8d4..553d5060 100644 --- a/web/src/lib/i18n/locales/pl.json +++ b/web/src/lib/i18n/locales/pl.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Komentarz} other {Komentarze}}", "completed": "Zakończono", "completion-status": "Stan ukończenia", + "confirm": "", + "confirm-share": "", "contribute": "Kontrybuuj", "copy-link": "", "create-new-list": "Stwórz nową listę", @@ -71,6 +73,7 @@ "error-exporting-trail": "", "error-printing-map": "", "error-reading-file": "Błąd wczytywania pliku", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "Szacowany czas", @@ -106,7 +109,10 @@ "license": "Licencja", "link-copied": "", "list": "{n, plural, =1 {Lista} other {Listy}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "Lokalizacja", "login": "Zaloguj się", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "Wybierz Listę", "settings": "Ustawienia", "share": "", + "share-this-list": "", "share-this-trail": "", "shared-by": "", "shared-with": "", diff --git a/web/src/lib/i18n/locales/pt.json b/web/src/lib/i18n/locales/pt.json index c523fcca..19b61a4a 100644 --- a/web/src/lib/i18n/locales/pt.json +++ b/web/src/lib/i18n/locales/pt.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {Comentário} other {Comentários}}", "completed": "Completada", "completion-status": "Status de conclusão", + "confirm": "", + "confirm-share": "", "contribute": "Contribuir", "copy-link": "", "create-new-list": "Criar nova lista", @@ -71,6 +73,7 @@ "error-exporting-trail": "", "error-printing-map": "", "error-reading-file": "Erro ao ler o arquivo", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "Duração prevista", @@ -106,7 +109,10 @@ "license": "Licença", "link-copied": "", "list": "{n, plural, =1 {Lista} other {Listas}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "Localização", "login": "Login", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "Selecionar lista", "settings": "Definições", "share": "", + "share-this-list": "", "share-this-trail": "", "shared-by": "", "shared-with": "", diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index aea0cfc8..906eeb84 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -31,6 +31,8 @@ "comment": "{n, plural, =1 {评论} other {评论}}", "completed": "已完成", "completion-status": "完成状态", + "confirm": "", + "confirm-share": "", "contribute": "贡献", "copy-link": "", "create-new-list": "创建新列表", @@ -71,6 +73,7 @@ "error-exporting-trail": "", "error-printing-map": "", "error-reading-file": "读取文件错误", + "error-saving-list": "", "error-saving-trail": "", "error-updating-password": "", "est-duration": "预计时长", @@ -106,7 +109,10 @@ "license": "开源协议", "link-copied": "", "list": "{n, plural, =1 {列表} other {列表}}", + "list-not-shared": "", "list-saved-successfully": "", + "list-share-warning": "", + "list-share-warning-update": "", "location": "地点", "login": "登录", "login-details": "", @@ -162,6 +168,7 @@ "select-list": "选择列表", "settings": "设置", "share": "", + "share-this-list": "", "share-this-trail": "", "shared-by": "", "shared-with": "", diff --git a/web/src/lib/models/list.ts b/web/src/lib/models/list.ts index 2b896af0..60c2861c 100644 --- a/web/src/lib/models/list.ts +++ b/web/src/lib/models/list.ts @@ -1,5 +1,6 @@ import { object, string } from "yup"; import type { Trail } from "./trail"; +import type { ListShare } from "./list_share"; export class List { id?: string; @@ -9,6 +10,8 @@ export class List { trails?: string[]; expand?: { trails: Trail[] + list_share_via_list?: ListShare[] + } author?: string; diff --git a/web/src/lib/models/list_share.ts b/web/src/lib/models/list_share.ts new file mode 100644 index 00000000..0c2399e3 --- /dev/null +++ b/web/src/lib/models/list_share.ts @@ -0,0 +1,17 @@ +import type { User } from "./user"; + +export class ListShare { + id?: string; + user: string; + list: string; + permission: "view" | "edit" + expand?: { + user: User + } + + constructor(user: string, list: string, permission: "view" | "edit") { + this.user = user; + this.list = list; + this.permission = permission + } +} \ No newline at end of file diff --git a/web/src/lib/stores/list_share_store.ts b/web/src/lib/stores/list_share_store.ts new file mode 100644 index 00000000..7c8da279 --- /dev/null +++ b/web/src/lib/stores/list_share_store.ts @@ -0,0 +1,57 @@ +import type { ListShare } from "$lib/models/list_share"; +import { ClientResponseError } from "pocketbase"; +import { writable, type Writable } from "svelte/store"; + +export const shares: Writable = writable([]) + + +export async function list_share_index(list: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { + const r = await f('/api/v1/list-share?' + new URLSearchParams({ + filter: `list='${list}'`, + }), { + method: 'GET', + }) + + if (!r.ok) { + throw new ClientResponseError(await r.json()) + } + + const response: ListShare[] = await r.json(); + + shares.set(response); + + return response; + +} + +export async function list_share_create(share: ListShare) { + let r = await fetch('/api/v1/list-share', { + method: 'PUT', + body: JSON.stringify(share), + }) + + if (!r.ok) { + throw new ClientResponseError(await r.json()) + } +} + +export async function list_share_update(share: ListShare) { + let r = await fetch('/api/v1/list-share/' + share.id, { + method: 'POST', + body: JSON.stringify(share), + }) + + if (!r.ok) { + throw new ClientResponseError(await r.json()) + } +} + +export async function list_share_delete(share: ListShare) { + const r = await fetch('/api/v1/list-share/' + share.id, { + method: 'DELETE', + }) + + if (!r.ok) { + throw new ClientResponseError(await r.json()) + } +} \ No newline at end of file diff --git a/web/src/lib/stores/trail_share_store.ts b/web/src/lib/stores/trail_share_store.ts index bc84506f..5ec08950 100644 --- a/web/src/lib/stores/trail_share_store.ts +++ b/web/src/lib/stores/trail_share_store.ts @@ -5,9 +5,9 @@ import { writable, type Writable } from "svelte/store"; export const shares: Writable = writable([]) -export async function trail_share_index(trail: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { +export async function trail_share_index(data: {trail?: string, user?: string}, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { const r = await f('/api/v1/trail-share?' + new URLSearchParams({ - filter: `trail='${trail}'`, + filter: data.trail ? `trail='${data.trail}'` : data.user ? `user='${data.user}'` : '', }), { method: 'GET', }) @@ -20,7 +20,7 @@ export async function trail_share_index(trail: string, f: (url: RequestInfo | UR shares.set(response); - return shares; + return response; } @@ -32,7 +32,7 @@ export async function trail_share_create(share: TrailShare) { if (!r.ok) { throw new ClientResponseError(await r.json()) - } + } } export async function trail_share_update(share: TrailShare) { diff --git a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js index b1ccf13b..296e6f21 100644 --- a/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js +++ b/web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js @@ -167,7 +167,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ _addTrack: function (track) { if (track instanceof Object) { this._loadGeoJSON(track); - } else { + } else if (track !== undefined) { this._elevation._parseFromString(track) .then(geojson => this._loadGeoJSON(geojson, this._hashCode(track), track.split('/').pop().split('#')[0].split('?')[0])) } @@ -226,7 +226,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ this._elevation.import([this._elevation.__LGEOMUTIL, this._elevation.__LDISTANCEM]).then(() => { - route.addTo(this._layers); + route.addTo(this._layers); route.eachLayer((layer) => this._onEachRouteLayer(route, layer)); @@ -355,17 +355,18 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({ // Ensure hash is a positive integer hash = Math.abs(hash); - // Define the maximum brightness (e.g., 200 ensures the color is not too light) - const maxBrightness = 200; + // Define the maximum brightness to ensure the color is not too light + const maxBrightness = 250; - // Convert the hash to a color in a reduced RGB range to prevent light colors + // Convert the hash to a color in a reduced RGB range to prevent light and green colors const r = (hash >> 16) & 0xFF; // Extract the red component const g = (hash >> 8) & 0xFF; // Extract the green component const b = hash & 0xFF; // Extract the blue component - // Adjust the RGB values to be within the allowed range (0 to maxBrightness) + // Adjust the green value to avoid greenish colors + // Reducing the green component significantly to avoid strong green shades + const green = Math.floor((g / 255) * (maxBrightness * 0.5)); // Reduce green component range const red = Math.floor((r / 255) * maxBrightness); - const green = Math.floor((g / 255) * maxBrightness); const blue = Math.floor((b / 255) * maxBrightness); // Format as HEX color diff --git a/web/src/routes/api/v1/list-share/+server.ts b/web/src/routes/api/v1/list-share/+server.ts new file mode 100644 index 00000000..e3b6432f --- /dev/null +++ b/web/src/routes/api/v1/list-share/+server.ts @@ -0,0 +1,36 @@ +import type { ListShare } from '$lib/models/list_share'; +import type { User } from '$lib/models/user'; +import { pb } from '$lib/pocketbase'; +import { error, json, type RequestEvent } from '@sveltejs/kit'; + +export async function GET(event: RequestEvent) { + const sort = event.url.searchParams.get('sort') ?? "" + const filter = event.url.searchParams.get("filter") ?? ""; + + try { + const r: ListShare[] = await pb.collection('list_share').getFullList({ + sort: sort, + filter: filter + }) + for (const share of r) { + const anonymous_user = await pb.collection('users_anonymous').getOne(share.user) + share.expand = { + user: anonymous_user + } + } + return json(r) + } catch (e: any) { + throw error(e.status, e); + } +} + +export async function PUT(event: RequestEvent) { + const data = await event.request.json(); + + try { + const r = await pb.collection('list_share').create(data) + return json(r); + } catch (e: any) { + throw error(e.status, e) + } +} \ No newline at end of file diff --git a/web/src/routes/api/v1/list-share/[id]/+server.ts b/web/src/routes/api/v1/list-share/[id]/+server.ts new file mode 100644 index 00000000..e3d7a9b1 --- /dev/null +++ b/web/src/routes/api/v1/list-share/[id]/+server.ts @@ -0,0 +1,33 @@ +import type { ListShare } from "$lib/models/list_share"; +import { pb } from "$lib/pocketbase"; +import { error, json, type RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent) { + try { + const r = await pb.collection('list_share') + .getOne(event.params.id as string) + return json(r) + } catch (e: any) { + throw error(e.status, e); + } +} + +export async function POST(event: RequestEvent) { + const data = await event.request.json() + + try { + const r = await pb.collection('list_share').update(event.params.id as string, data) + return json(r); + } catch (e: any) { + throw error(e.status, e) + } +} + +export async function DELETE(event: RequestEvent) { + try { + const r = await pb.collection('list_share').delete(event.params.id as string) + return json({ 'acknowledged': r }); + } catch (e: any) { + throw error(e.status, e) + } +} diff --git a/web/src/routes/api/v1/list/+server.ts b/web/src/routes/api/v1/list/+server.ts index 633ab7a6..5e42d72a 100644 --- a/web/src/routes/api/v1/list/+server.ts +++ b/web/src/routes/api/v1/list/+server.ts @@ -6,7 +6,7 @@ export async function GET(event: RequestEvent) { const sort = event.url.searchParams.get('sort') ?? "" try { const r: List[] = await pb.collection('lists').getFullList({ - expand: "trails,trails.waypoints,trails.category", + expand: "trails,trails.waypoints,trails.category,list_share_via_list", sort: sort, }) return json(r) diff --git a/web/src/routes/api/v1/list/[id]/+server.ts b/web/src/routes/api/v1/list/[id]/+server.ts index 793fe23a..52564b04 100644 --- a/web/src/routes/api/v1/list/[id]/+server.ts +++ b/web/src/routes/api/v1/list/[id]/+server.ts @@ -5,7 +5,7 @@ import { error, json, type RequestEvent } from "@sveltejs/kit"; export async function GET(event: RequestEvent) { try { const r = await pb.collection('lists') - .getOne(event.params.id as string, { expand: "trails,trails.waypoints,trails.category" }) + .getOne(event.params.id as string, { expand: "trails,trails.waypoints,trails.category,list_share_via_list" }) return json(r) } catch (e: any) { throw error(e.status, e); diff --git a/web/src/routes/lists/+page.svelte b/web/src/routes/lists/+page.svelte index 86aafdcc..62d38b54 100644 --- a/web/src/routes/lists/+page.svelte +++ b/web/src/routes/lists/+page.svelte @@ -5,6 +5,7 @@ import ConfirmModal from "$lib/components/confirm_modal.svelte"; import ListCard from "$lib/components/list/list_card.svelte"; import ListModal from "$lib/components/list/list_modal.svelte"; + import ListShareModal from "$lib/components/list/list_share_modal.svelte"; import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte"; import TrailList from "$lib/components/trail/trail_list.svelte"; import { List } from "$lib/models/list"; @@ -28,6 +29,7 @@ let openListModal: () => void; let openConfirmModal: () => void; + let openShareModal: () => void; let filter: TrailFilter = $page.data.filter; let listToBeDeleted: List | null = null; @@ -60,7 +62,11 @@ ) { const item = e.detail; - if (item.value == "edit") { + if (item.value == "share") { + list.set(currentList); + await tick(); + openShareModal(); + } else if (item.value == "edit") { goto("/lists/edit/" + currentList.id); } else if (item.value == "delete") { openConfirmModal(); @@ -91,19 +97,14 @@ {$_("list", { values: { n: 2 } })} | wanderer -
+
    - {$_("new-list")}
-
+
+
diff --git a/web/src/routes/lists/edit/[id]/+page.svelte b/web/src/routes/lists/edit/[id]/+page.svelte index b5603338..38ff02a8 100644 --- a/web/src/routes/lists/edit/[id]/+page.svelte +++ b/web/src/routes/lists/edit/[id]/+page.svelte @@ -12,6 +12,8 @@ import Textarea from "$lib/components/base/textarea.svelte"; import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte"; import type { Trail } from "$lib/models/trail.js"; + import { lists_create, lists_update } from "$lib/stores/list_store.js"; + import { show_toast } from "$lib/stores/toast_store.js"; import { trails_show } from "$lib/stores/trail_store"; import { getFileURL } from "$lib/util/file_util.js"; import { @@ -19,13 +21,16 @@ formatElevation, formatTimeHHMM, } from "$lib/util/format_util"; - import { lists_create, lists_update } from "$lib/stores/list_store.js"; - import { show_toast } from "$lib/stores/toast_store.js"; - import { onMount } from "svelte"; + import { + trail_share_create, + trail_share_index, + } from "$lib/stores/trail_share_store.js"; + import { TrailShare } from "$lib/models/trail_share.js"; + import ConfirmModal from "$lib/components/confirm_modal.svelte"; export let data; - let previewURL = ""; + let previewURL = data.previewUrl ?? ""; let searchDropdownItems: SearchItem[] = []; let activeTrailIndex: number | null = null; @@ -34,6 +39,10 @@ let loading: boolean = false; + let newShares: TrailShare[] = []; + + let openConfirmModal: () => void; + const { form, errors, handleChange, handleSubmit } = createForm({ initialValues: data.list!, validationSchema: listSchema, @@ -42,18 +51,34 @@ document.getElementById("avatar") as HTMLInputElement ).files![0]; loading = true; - if ($form.id) { - await lists_update($form, avatarFile); - } else { - await lists_create($form, avatarFile); + try { + if ($form.id) { + await lists_update($form, avatarFile); + await findNewTrailShares(); + if (!newShares.length) { + show_toast({ + type: "success", + icon: "check", + text: $_("list-saved-successfully"), + }); + } + } else { + await lists_create($form, avatarFile); + show_toast({ + type: "success", + icon: "check", + text: $_("list-saved-successfully"), + }); + } + } catch (e) { + show_toast({ + type: "error", + icon: "close", + text: $_("error-saving-list"), + }); + } finally { + loading = false; } - loading = false; - - show_toast({ - type: "success", - icon: "check", - text: $_("list-saved-successfully"), - }); }, }); @@ -88,14 +113,14 @@ const response = await r.json(); - searchDropdownItems = response.results[0].hits.map( - (t: Record) => ({ + searchDropdownItems = response.results[0].hits + .filter((h: List) => !$form.trails?.includes(h.id!)) + .map((t: Record) => ({ text: t.name, description: `${t.location ?? "-"}`, value: t.id, icon: "route", - }), - ); + })); } async function handleSearchClick(item: SearchItem) { @@ -110,12 +135,57 @@ (t) => t.id !== trail.id, ); } + + async function findNewTrailShares() { + const usersWithAccess: string[] = [ + $form.author!, + ...($form.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 ?? []) { + if (trail.author == userId) { + continue; + } + const trailShare = existingTrailShares.find( + (s) => s.trail == trail.id, + ); + if (!trailShare) { + newShares.push(new TrailShare(userId, trail.id!, "view")); + } + } + } + + if (newShares.length) { + openConfirmModal(); + } + } + + async function updateTrailShares() { + for (const newShare of newShares) { + await trail_share_create(newShare); + } + newShares = []; + show_toast({ + type: "success", + icon: "check", + text: $_("list-saved-successfully"), + }); + } + + {$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer +

@@ -248,10 +318,28 @@ {loading}>{$_("save-list")} - +
+ +

+ + + + diff --git a/web/src/routes/lists/edit/[id]/+page.ts b/web/src/routes/lists/edit/[id]/+page.ts index 1a40da3b..4ec45393 100644 --- a/web/src/routes/lists/edit/[id]/+page.ts +++ b/web/src/routes/lists/edit/[id]/+page.ts @@ -1,5 +1,6 @@ import { List } from "$lib/models/list"; import { lists_show } from "$lib/stores/list_store"; +import { getFileURL } from "$lib/util/file_util"; import { error, type Load } from "@sveltejs/kit"; import { ClientResponseError } from "pocketbase"; @@ -11,11 +12,13 @@ export const load: Load = async ({ params, fetch, data }) => { let list: List; if (params.id === "new") { list = new List("", []); - return { list: list } + return { list: list, previewUrl: "" } } else { try { list = await lists_show(params.id, fetch); - return { list: list } + const previewURL = getFileURL(list, list.avatar); + + return { list: list, previewUrl: previewURL } } catch (e) { if (e instanceof ClientResponseError) { diff --git a/web/src/routes/map/trail/[id]/+page.svelte b/web/src/routes/map/trail/[id]/+page.svelte index bca934cc..5a4ce608 100644 --- a/web/src/routes/map/trail/[id]/+page.svelte +++ b/web/src/routes/map/trail/[id]/+page.svelte @@ -17,7 +17,7 @@
-
+