adds trail link sharing

This commit is contained in:
Christian Beutel
2025-08-20 16:51:33 +02:00
parent b3d44f3b78
commit 1e22c78c22
36 changed files with 598 additions and 36 deletions

View File

@@ -0,0 +1,116 @@
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"
},
{
"cascadeDelete": true,
"collectionId": "e864strfxo14pm4",
"hidden": false,
"id": "relation2993194383",
"maxSelect": 1,
"minSelect": 0,
"name": "trail",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "[a-z0-9]{32}",
"hidden": false,
"id": "text1597481275",
"max": 32,
"min": 32,
"name": "token",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "select3762918058",
"maxSelect": 1,
"name": "permission",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"view",
"edit"
]
},
{
"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_2865004035",
"indexes": [],
"listRule": null,
"name": "trail_link_share",
"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_2865004035")
if err != nil {
return err
}
return app.Delete(collection)
})
}

View 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_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_YbuWvsh5la` + "`" + ` ON ` + "`" + `trail_link_share` + "`" + ` (` + "`" + `trail` + "`" + `)"
]
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"indexes": []
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -0,0 +1,48 @@
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_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": "trail.author.user = @request.auth.id",
"deleteRule": "trail.author.user = @request.auth.id",
"listRule": "trail.author.user = @request.auth.id",
"updateRule": "trail.author.user = @request.auth.id",
"viewRule": "trail.author.user = @request.auth.id"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("pbc_2865004035")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"createRule": null,
"deleteRule": null,
"listRule": null,
"updateRule": null,
"viewRule": null
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View 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("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id) || trail_link_share_via_trail.token = @request.query.share "
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id)",
"viewRule": "author.user = @request.auth.id || public = true || (@request.auth.id != \"\" && trail_share_via_trail.actor.user ?= @request.auth.id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View 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("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)\n|| \n(@collection.trail_link_share.trail.id ?= trails_via_waypoints.id && @collection.trail_link_share.token = @request.query.share)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("goeo2ubp103rzp9")
if err != nil {
return err
}
// update collection data
if err := json.Unmarshal([]byte(`{
"listRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)",
"viewRule": "author = @request.auth.id || trails_via_waypoints.author.user ?= @request.auth.id || trails_via_waypoints.public ?= true || \n(@collection.trail_share.trail.id ?= trails_via_waypoints.id && @collection.trail_share.actor.user ?= @request.auth.id)"
}`), &collection); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@@ -22,6 +22,7 @@
import TrailShareModal from "./trail_share_modal.svelte";
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
import type { Snippet } from "svelte";
import { page } from "$app/state";
interface Props {
trails?: Set<Trail> | undefined;
@@ -44,6 +45,7 @@
return (
hasTrail() &&
!isMultiselectMode() &&
Boolean($currentUser) &&
(trail()!.expand?.author?.id === $currentUser?.actor ||
trail()!.expand?.trail_share_via_trail?.some(
(s) => s.permission == "edit",
@@ -143,6 +145,9 @@
}
function isFromCurrentUser(uTrail?: Trail): boolean {
if (!$currentUser) {
return false;
}
if (uTrail !== undefined) {
return uTrail.expand?.author?.id === $currentUser?.actor;
} else if (trails !== undefined && trails.size > 0) {
@@ -169,14 +174,16 @@
return;
}
const handle = handleFromRecordWithIRI(trail());
const handle = page.params.handle
if (item.value == "show") {
if (hasTrail()) {
goto(
mode == "overview" || mode == "multi-select"
const url = mode == "overview" || mode == "multi-select"
? `/map/trail/${handle}/${trailId()}`
: `/trail/view/${handle}/${trailId()}`,
: `/trail/view/${handle}/${trailId()}`
goto(
url + '?' + page.url.searchParams
);
}
} else if (item.value == "list") {
@@ -199,7 +206,7 @@
}
} else if (item.value == "print") {
if (hasTrail()) {
goto(`/map/trail/${handle}/${trailId()}/print`);
goto(`/map/trail/${handle}/${trailId()}/print?${page.url.searchParams}`);
}
} else if (item.value == "share") {
trailShareModal.openModal();

View File

@@ -398,19 +398,17 @@
</h3>
</div>
</div>
{#if ($currentUser && $currentUser.actor == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public}
<div class="flex flex-col items-center gap-y-2">
<div class="flex flex-col items-center gap-y-2">
{#if ($currentUser && $currentUser.actor == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public}
<LikeButton {trail}></LikeButton>
<TrailDropdown
trails={new Set<Trail>([trail])}
onDelete={() =>
history.length
? history.back()
: goto("/trails")}
{mode}
></TrailDropdown>
</div>
{/if}
{/if}
<TrailDropdown
trails={new Set<Trail>([trail])}
onDelete={() =>
history.length ? history.back() : goto("/trails")}
{mode}
></TrailDropdown>
</div>
</div>
</section>
<section
@@ -520,7 +518,7 @@
<div class="mb-6 mt-12 flex justify-between flex-wrap gap-y-4">
<Tabs {tabs} bind:activeTab></Tabs>
{#if activeTab == 0 && mode != "list"}
{#if $currentUser && activeTab == 0 && mode != "list"}
<Button
secondary
type="button"

View File

@@ -14,6 +14,15 @@
import Button from "../base/button.svelte";
import type { SelectItem } from "../base/select.svelte";
import Select from "../base/select.svelte";
import type { RadioItem } from "../base/radio_group.svelte";
import RadioGroup from "../base/radio_group.svelte";
import { TrailLinkShare } from "$lib/models/trail_link_share";
import {
linkShares,
trail_link_share_create,
trail_link_share_delete,
trail_link_share_index,
} from "$lib/stores/trail_link_share_store";
interface Props {
trail?: Trail;
@@ -30,8 +39,8 @@
openShareModalLocal();
}
async function openShareModalLocal() {
modal.openModal();
await fetchShares();
modal.openModal();
}
let copyButtonText = $state($_("copy-link"));
@@ -43,8 +52,25 @@
{ text: $_("edit"), value: "edit" },
];
const publicShareItems: RadioItem[] = [
{
text: $_("limited"),
value: "limited",
description: $_("public-share-limited"),
},
{
text: $_("everyone-with-the-link"),
value: "everyone",
description: $_("public-share-everyone"),
},
];
function copyURLToClipboard() {
navigator.clipboard.writeText(window.location.href);
let link = window.location.href;
if ($linkShares.length > 0) {
link = link + "?share=" + $linkShares[0].token;
}
navigator.clipboard.writeText(link);
copyButtonText = $_("link-copied");
setTimeout(() => (copyButtonText = $_("copy-link")), 3000);
@@ -88,8 +114,24 @@
sharesLoading = true;
await trail_share_index({ trail: trail.id! });
await trail_link_share_index(trail.id);
sharesLoading = false;
}
async function handlePublicAccessChange(e: RadioItem) {
if (trail === undefined) {
return;
}
if (e.value === "everyone") {
const share = new TrailLinkShare(trail.id!, "view");
await trail_link_share_create(share);
} else if (e.value === "limited" && $linkShares.length > 0) {
const share = $linkShares[0];
await trail_link_share_delete(share);
}
await trail_link_share_index(trail.id);
}
</script>
<Modal
@@ -159,6 +201,14 @@
{/if}
{/each}
{/if}
<h4 class="font-semibold mt-4 mb-2">{$_("public-access")}</h4>
<RadioGroup
name="account"
items={publicShareItems}
selected={$linkShares.length == 0 ? 0 : 1}
onchange={handlePublicAccessChange}
></RadioGroup>
</div>
{/snippet}
{#snippet footer()}

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Fehler beim Aktualisieren des Passworts",
"error-updating-strava-integration": "Fehler bei Aktualisierung der Komoot-Integration",
"est-duration": "Gesch. Dauer",
"everyone-with-the-link": "",
"explore": "Erkunden",
"explore-some-trails": "Erkunde einige Routen",
"export": "Exportieren",
@@ -215,6 +216,7 @@
"like-status": "\"Gefällt mir\" Status",
"liked": "Gefällt mir",
"likes": "Likes",
"limited": "",
"link-copied": "Link kopiert!",
"list": "{n, plural, =1 {Liste} other {Listen}}",
"list-not-shared": "Mit niemandem geteilt",
@@ -311,6 +313,9 @@
"private": "Privat",
"profile": "Profil",
"public": "Öffentlich",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Radius",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Error updating password",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "Est. duration",
"everyone-with-the-link": "Everyone with the link",
"explore": "Explore",
"explore-some-trails": "Explore some trails",
"export": "Export",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "Limited",
"link-copied": "Link copied!",
"list": "{n, plural, =1 {List} other {Lists}}",
"list-not-shared": "Not shared with anyone",
@@ -311,6 +313,9 @@
"private": "Private",
"profile": "Profile",
"public": "Public",
"public-access": "Public access",
"public-share-everyone": "Everyone on the internet with the link can see this trail",
"public-share-limited": "Only people with access can open the link",
"public-transport": "Public transport",
"radius": "Radius",
"railway-station": "Railway station",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Error actualizando la contraseña",
"error-updating-strava-integration": "Error al actualizar la integración con komoot",
"est-duration": "Duración estimada",
"everyone-with-the-link": "",
"explore": "Explora",
"explore-some-trails": "Explora alguna ruta",
"export": "Exportar",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "",
"link-copied": "¡Enlace copiado!",
"list": "{n, plural, one {}=1 {Lista} other {Listas}}",
"list-not-shared": "No compartido con ninguno",
@@ -311,6 +313,9 @@
"private": "Privado",
"profile": "Perfil",
"public": "Público",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Radio",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Error updating password",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "Est. duration",
"everyone-with-the-link": "",
"explore": "Explore",
"explore-some-trails": "Explore some trails",
"export": "Export",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "",
"link-copied": "Link copied!",
"list": "{n, plural, =1 {List} other {Lists}}",
"list-not-shared": "Not shared with anyone",
@@ -311,6 +313,9 @@
"private": "Private",
"profile": "Profile",
"public": "Public",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Radius",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Erreur lors de la mise à jour du mot de passe",
"error-updating-strava-integration": "Erreur lors de la mise à jour de l'intégration Komoot",
"est-duration": "Temps estimé",
"everyone-with-the-link": "",
"explore": "Explorer",
"explore-some-trails": "Explorer les itinéraires",
"export": "Exporter",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Aimé",
"likes": "Likes",
"limited": "",
"link-copied": "Lien copié !",
"list": "{n, plural, =1 {Liste} other {Listes}}",
"list-not-shared": "Non partagé avec quiconque",
@@ -311,6 +313,9 @@
"private": "Privé",
"profile": "Profile",
"public": "Publique",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Rayon",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Error updating password",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "Becsült időtartam",
"everyone-with-the-link": "",
"explore": "Felfedezés",
"explore-some-trails": "Fedezzen fel néhány ösvényt",
"export": "Export",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "",
"link-copied": "Link copied!",
"list": "{n, plural, =1 {Lista} other {Listák}}",
"list-not-shared": "Not shared with anyone",
@@ -311,6 +313,9 @@
"private": "Private",
"profile": "Profil",
"public": "Publikus",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Átmérő",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Errore nell'aggiornamento della password",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "Durata stimata",
"everyone-with-the-link": "",
"explore": "Esplora",
"explore-some-trails": "Esplora alcuni percorsi",
"export": "Esporta",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "",
"link-copied": "Link copiato",
"list": "{n, plural, =1 {Lista} other {Liste}}",
"list-not-shared": "Non condivisa",
@@ -311,6 +313,9 @@
"private": "Privato",
"profile": "Profilo",
"public": "Pubblico",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Raggio",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Fout bij bijwerken van wachtwoord",
"error-updating-strava-integration": "Fout bij bijwerken van Komoot integratie",
"est-duration": "Geschatte duur",
"everyone-with-the-link": "",
"explore": "Verkennen",
"explore-some-trails": "Verken enkele routes",
"export": "Exporteer",
@@ -215,6 +216,7 @@
"like-status": "",
"liked": "Leuk gevonden",
"likes": "Vind-ik-leuks",
"limited": "",
"link-copied": "Link gekopieerd",
"list": "{n, plural, =1 {Lijst} other {Lijsten}}",
"list-not-shared": "Niet gedeeld met iemand",
@@ -311,6 +313,9 @@
"private": "Privaat",
"profile": "Profiel",
"public": "Openbaar",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Straal",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Błąd podczas aktualizacji hasła",
"error-updating-strava-integration": "Błąd aktualizacji integracji kamoot",
"est-duration": "Szacowany czas",
"everyone-with-the-link": "",
"explore": "Eksploruj",
"explore-some-trails": "Eksploruj różne szlaki",
"export": "Eksportuj",
@@ -215,6 +216,7 @@
"like-status": "Status polubień",
"liked": "Polubiony",
"likes": "Polubienia",
"limited": "",
"link-copied": "Link skopiowany!",
"list": "{n, plural, =1 {Lista} other {Listy}}",
"list-not-shared": "Nikomu nie udostępniony",
@@ -311,6 +313,9 @@
"private": "Prywatne",
"profile": "Profil",
"public": "Publiczny",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Promień",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Erro ao atualizar password",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "Duração prevista",
"everyone-with-the-link": "",
"explore": "Explorar",
"explore-some-trails": "Explore algumas trilhas",
"export": "Exportar",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "",
"link-copied": "Link copiado!",
"list": "{n, plural, =1 {Lista} other {Listas}}",
"list-not-shared": "Não partilhado com ninguém",
@@ -311,6 +313,9 @@
"private": "Private",
"profile": "Perfil",
"public": "Público",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Raio",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "Ошибка изменения пароля",
"error-updating-strava-integration": "Ошибка обновления Strava",
"est-duration": "Продолжительность",
"everyone-with-the-link": "",
"explore": "Изучить",
"explore-some-trails": "Изучите треки",
"export": "Экспорт",
@@ -215,6 +216,7 @@
"like-status": "Статус лайка",
"liked": "Лайкнуто",
"likes": "Лайков",
"limited": "",
"link-copied": "Ссылка скопирована!",
"list": "{n, plural, =1 {Список} other {Списки}}",
"list-not-shared": "Нет общего доступа",
@@ -311,6 +313,9 @@
"private": "Зкрытый",
"profile": "Профиль",
"public": "Открытый",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "Радиус",
"railway-station": "",

View File

@@ -147,6 +147,7 @@
"error-updating-password": "更新密码失败",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "预计时长",
"everyone-with-the-link": "",
"explore": "探索",
"explore-some-trails": "探索行程",
"export": "导出",
@@ -215,6 +216,7 @@
"like-status": "Like Status",
"liked": "Liked",
"likes": "Likes",
"limited": "",
"link-copied": "链接已复制",
"list": "{n, plural, =1 {列表} other {列表}}",
"list-not-shared": "未与任何人分享",
@@ -311,6 +313,9 @@
"private": "Private",
"profile": "个人资料",
"public": "公开",
"public-access": "",
"public-share-everyone": "",
"public-share-limited": "",
"public-transport": "",
"radius": "半径",
"railway-station": "",

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
const RecordOptionsSchema = z.object({
expand: z.string().optional(),
query: z.record(z.string()).optional(),
requestKey: z.string().optional()
})

View File

@@ -0,0 +1,16 @@
import { z, ZodType } from "zod";
import type { TrailShare } from "../trail_share";
import type { TrailLinkShare } from "../trail_link_share";
const TrailLinkShareCreateSchema = z.object({
trail: z.string().length(15),
permission: z.enum(["view", "edit"])
}) satisfies ZodType<TrailLinkShare>
const TrailLinkShareUpdateSchema = z.object({
permission: z.enum(["view", "edit"])
}) satisfies ZodType<Partial<TrailShare>>
export { TrailLinkShareCreateSchema, TrailLinkShareUpdateSchema };

View File

@@ -0,0 +1,13 @@
export class TrailLinkShare {
id?: string;
trail: string;
token?: string;
permission: "view" | "edit"
constructor(trail: string, permission: "view" | "edit") {
this.trail = trail;
this.permission = permission
}
}

View File

@@ -0,0 +1,66 @@
import type { TrailLinkShare } from "$lib/models/trail_link_share";
import { APIError } from "$lib/util/api_util";
import { type ListResult } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const linkShares: Writable<TrailLinkShare[]> = writable([])
export async function trail_link_share_index(trail?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
let filter = ""
filter += `trail='${trail}'`
const r = await f('/api/v1/trail-link-share?' + new URLSearchParams({
filter: filter,
expand: "actor"
}), {
method: 'GET',
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
const response: ListResult<TrailLinkShare> = await r.json();
linkShares.set(response.items);
return response.items;
}
export async function trail_link_share_create(share: TrailLinkShare) {
let r = await fetch('/api/v1/trail-link-share', {
method: 'PUT',
body: JSON.stringify(share),
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
}
export async function trail_link_share_update(share: TrailLinkShare) {
let r = await fetch('/api/v1/trail-link-share/' + share.id, {
method: 'POST',
body: JSON.stringify(share),
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
}
export async function trail_link_share_delete(share: TrailLinkShare) {
const r = await fetch('/api/v1/trail-link-share/' + share.id, {
method: 'DELETE',
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
}

View File

@@ -133,12 +133,12 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
}
export async function trails_show(id: string, handle?: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
export async function trails_show(id: string, handle?: string, share?: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
expand: "category,waypoints,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author",
...(handle ? { handle } : {})
...(handle ? { handle } : {}),
...(share ? { share } : {})
}), {
method: 'GET',
})

View File

@@ -34,6 +34,7 @@ export enum Collection {
summit_logs = "summit_logs",
trail_like = "trail_like",
trail_share = "trail_share",
trail_link_share = "trail_link_share",
trails = "trails",
tags = "tags",
waypoints = "waypoints",

View File

@@ -81,7 +81,7 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
if (!trail.expand?.gpx_data) {
// no gpx_data -> empty trail?
// or just not expanded? -> expand now
const response = await trails_show(trail.id!, handleFromRecordWithIRI(trail), true);
const response = await trails_show(trail.id!, handleFromRecordWithIRI(trail), undefined, true);
if (!response.expand?.gpx_data) {
throw Error("Trail has no GPX data")

View File

@@ -0,0 +1,26 @@
import { TrailLinkShareCreateSchema } from '$lib/models/api/trail_link_share_schema';
import type { TrailLinkShare } from '$lib/models/trail_link_share';
import { Collection, 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<TrailLinkShare>(event, Collection.trail_link_share);
return json(r)
} catch (e: any) {
handleError(e)
}
}
export async function PUT(event: RequestEvent) {
try {
const data = await event.request.json();
const safeData = TrailLinkShareCreateSchema.parse(data);
const r = await event.locals.pb.collection(Collection.trail_link_share).create<TrailLinkShare>(safeData)
return json(r);
} catch (e) {
return handleError(e)
}
}

View File

@@ -0,0 +1,31 @@
import { TrailLinkShareUpdateSchema } from "$lib/models/api/trail_link_share_schema";
import type { TrailLinkShare } from "$lib/models/trail_link_share";
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<TrailLinkShare>(event, Collection.trail_link_share)
return json(r)
} catch (e: any) {
return handleError(e)
}
}
export async function POST(event: RequestEvent) {
try {
const r = await update<TrailLinkShare>(event, TrailLinkShareUpdateSchema, Collection.trail_link_share)
return json(r);
} catch (e: any) {
return handleError(e)
}
}
export async function DELETE(event: RequestEvent) {
try {
const r = await remove(event, Collection.trail_link_share)
return json(r);
} catch (e: any) {
return handleError(e)
}
}

View File

@@ -18,7 +18,9 @@ export async function GET(event: RequestEvent) {
event.url.searchParams.delete("handle")
const safeSearchParams = RecordOptionsSchema.parse(Object.fromEntries(event.url.searchParams));
if (event.url.searchParams.has("share")) {
safeSearchParams.query = { share: event.url.searchParams.get("share")! }
}
let origin = new URL(actor.iri).origin
let iri = `${origin}/api/v1/trail/${event.params.id}`

View File

@@ -137,6 +137,7 @@
const fullTrail = await trails_show(
trail.iri ? trail.iri.substring(trail.iri.length - 15) : trail.id!,
handleFromRecordWithIRI(trail),
undefined,
true,
);
selectedTrail = fullTrail;
@@ -251,7 +252,8 @@
disabled={!selectedList}
onclick={back}><i class="fa fa-arrow-left"></i></button
>
<Search bind:value={filter.q} onupdate={() => updateFilter()}></Search>
<Search bind:value={filter.q} onupdate={() => updateFilter()}
></Search>
<button
aria-label="Toggle filter"
class="btn-icon"

View File

@@ -187,7 +187,7 @@
}
async function handleSearchClick(item: SearchItem) {
const trail = await trails_show(item.value, undefined, true);
const trail = await trails_show(item.value, undefined, undefined, true);
$formData.trails?.push(trail.id!);
$formData.expand = {
trails: [...($formData.expand?.trails ?? []), trail],

View File

@@ -1,11 +1,10 @@
import { trails_show } from "$lib/stores/trail_store";
import { splitUsername } from "$lib/util/activitypub_util";
import { APIError } from "$lib/util/api_util";
import { error, type NumericRange, type ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ params, locals, fetch }) => {
export const load: ServerLoad = async ({ params, url, fetch }) => {
try {
const trail = await trails_show(params.id!, params.handle, true, fetch)
const trail = await trails_show(params.id!, params.handle, url.searchParams.get("share") ?? undefined, true, fetch)
return { trail };
} catch (e) {
if (e instanceof APIError) {

View File

@@ -2,9 +2,9 @@ import { trails_show } from "$lib/stores/trail_store";
import { APIError } from "$lib/util/api_util";
import { error, type Load, type NumericRange } from "@sveltejs/kit";
export const load: Load = async ({ params, fetch }) => {
export const load: Load = async ({ params, fetch, url }) => {
try {
const trail = await trails_show(params.id!, params.handle, true, fetch)
const trail = await trails_show(params.id!, params.handle, url.searchParams.get("share") ?? undefined, true, fetch)
return { trail };
} catch (e) {
if (e instanceof APIError) {

View File

@@ -6,7 +6,7 @@ import { currentUser } from "$lib/stores/user_store";
import { error, type Load } from "@sveltejs/kit";
import { get } from "svelte/store";
export const load: Load = async ({ params, fetch }) => {
export const load: Load = async ({ params, fetch, url }) => {
const user = get(currentUser)
if (!params.id) {
@@ -19,7 +19,7 @@ export const load: Load = async ({ params, fetch }) => {
if (params.id === "new") {
trail = new Trail("", { category: categories[0] });
} else {
trail = await trails_show(params.id, undefined, true, fetch);
trail = await trails_show(params.id, undefined, url.searchParams.get("share") ?? undefined, true, fetch);
}
return { trail: trail, lists: lists }

View File

@@ -2,9 +2,9 @@ import { trails_show } from "$lib/stores/trail_store";
import { APIError } from "$lib/util/api_util";
import { error, type Load, type NumericRange } from "@sveltejs/kit";
export const load: Load = async ({ params, fetch }) => {
export const load: Load = async ({ params, fetch, url }) => {
try {
const trail = await trails_show(params.id!, params.handle, true, fetch)
const trail = await trails_show(params.id!, params.handle, url.searchParams.get("share") ?? undefined, true, fetch)
return { trail }
} catch (e) {