implements trail sharing

This commit is contained in:
Christian Beutel
2024-06-06 02:02:23 +02:00
parent 5d46ad96b4
commit ad5a4e668b
44 changed files with 1257 additions and 308 deletions

View File

@@ -8,6 +8,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { fly } from "svelte/transition";
export let items: DropdownItem[] = [];
export let size: string = "regular";
@@ -30,26 +31,38 @@
dispatch("change", item);
closeMenu();
}
function handleWindowClick(e: MouseEvent) {
if ((e.target as HTMLElement).parentElement?.classList.contains("dropdown-toggle")) {
return;
}
isOpen = false;
}
</script>
<svelte:window on:mouseup={() => (isOpen = false)} />
<svelte:window on:mouseup={handleWindowClick} />
<div class="dropdown relative">
<slot {toggleMenu}>
<button
class="btn-icon flex items-center justify-center"
on:click={toggleMenu}
type="button"
>
<i class="fa fa-ellipsis-vertical text-{size}"></i>
</button>
</slot>
<div class="dropdown-toggle">
<slot {toggleMenu}>
<button
class="btn-icon flex items-center justify-center"
on:click={toggleMenu}
type="button"
>
<i class="fa fa-ellipsis-vertical text-{size}"></i>
</button>
</slot>
</div>
{#if isOpen}
<ul
class="menu absolute bg-menu-background border border-input-border rounded-l-xl rounded-b-xl shadow-md right-0 overflow-hidden mt-2"
class:none={isOpen}
style="z-index: 1001"
in:fly={{ y: -10, duration: 150 }}
out:fly={{ y: -10, duration: 150 }}
>
{#each items as item}
<li

View File

@@ -28,7 +28,7 @@
class="relative w-full {size} max-h-full rounded-xl text-content"
>
<!-- Modal content -->
<div class="relative bg-background shadow">
<div class="relative bg-background shadow rounded-xl">
<!-- Modal header -->
<div
class="flex items-center justify-between p-4 md:p-5 border-b border-separator rounded-t"

View File

@@ -97,7 +97,9 @@
on:mousedown|stopPropagation={() => handleItemClick(item)}
on:keydown|stopPropagation={() => handleItemClick(item)}
>
<i class="fa fa-{item.icon} mr-6"></i>
<slot name="item-header" {item}>
<i class="fa fa-{item.icon} mr-6"></i>
</slot>
<div>
<p>{item.text}</p>

View File

@@ -8,6 +8,7 @@
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import TrailShareInfo from "./trail_share_info.svelte";
export let trail: Trail;
@@ -22,20 +23,25 @@
on:mouseleave
role="listitem"
>
{#if trail.public}
<div
class=" top-4 right-4 z-10 tooltip"
style="position: absolute!important"
data-title={$_("public")}
>
<i class="fa fa-globe"></i>
</div>
{/if}
<div
class="relative w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl"
>
<img src={thumbnail} alt="" />
</div>
{#if trail.public || trail.expand?.trail_share_via_trail?.length}
<div
class="flex absolute top-4 right-4 w-8 h-8 rounded-full items-center justify-center bg-background text-content"
>
{#if trail.public}
<span class="tooltip" data-title={$_("public")}>
<i class="fa fa-globe"></i>
</span>
{/if}
{#if trail.expand?.trail_share_via_trail?.length}
<TrailShareInfo {trail}></TrailShareInfo>
{/if}
</div>
{/if}
<div class="p-4">
<div>
<h4 class="font-semibold text-lg">{trail.name}</h4>
@@ -45,7 +51,7 @@
month: "long",
day: "2-digit",
year: "numeric",
timeZone: 'UTC'
timeZone: "UTC",
})}
</p>
{/if}
@@ -65,12 +71,12 @@
<div class="flex mt-1 gap-4 text-sm text-gray-500 whitespace-nowrap">
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance
trail.distance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
trail.elevation_gain
trail.elevation_gain,
)}</span
>
<span

View File

@@ -19,6 +19,8 @@
import JSZip from "jszip";
import { getFileURL, saveAs } from "$lib/util/file_util";
import TrailShareModal from "./trail_share_modal.svelte";
import { currentUser } from "$lib/stores/user_store";
import { pb } from "$lib/pocketbase";
export let trail: Trail;
export let mode: "overview" | "map";
@@ -28,6 +30,10 @@
let openExportModal: () => void;
let openShareModal: () => void;
const allowEdit =
trail.author == $currentUser?.id ||
trail.expand.trail_share_via_trail?.some((s) => s.permission == "edit");
const dropdownItems: DropdownItem[] = [
mode == "overview"
? { text: $_("show-on-map"), value: "show", icon: "map" }
@@ -49,9 +55,15 @@
: []),
{ text: $_("print"), value: "print", icon: "print" },
{ text: $_("add-to-list"), value: "list", icon: "bookmark" },
{ text: $_("share"), value: "share", icon: "share" },
{ text: $_("edit"), value: "edit", icon: "pen" },
{ text: $_("delete"), value: "delete", icon: "trash" },
...(trail.author != pb.authStore.model?.id
? []
: [{ text: $_("share"), value: "share", icon: "share" }]),
...(allowEdit
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
: []),
...(trail.author == $currentUser?.id
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
: []),
];
async function handleDropdownClick(item: { text: string; value: any }) {
@@ -72,7 +84,7 @@
?.focus();
} else if (item.value == "print") {
goto(`/map/trail/${trail.id}/print`);
} else if (item.value == "share") {
} else if (item.value == "share") {
openShareModal();
} else if (item.value == "download") {
openExportModal();
@@ -189,4 +201,4 @@
bind:openModal={openExportModal}
on:export={(e) => exportTrail(e.detail)}
></TrailExportModal>
<TrailShareModal bind:openModal={openShareModal}></TrailShareModal>
<TrailShareModal {trail} bind:openShareModal></TrailShareModal>

View File

@@ -31,6 +31,7 @@
import Textarea from "../base/textarea.svelte";
import CommentCard from "../comment/comment_card.svelte";
import PhotoGallery from "../photo_gallery.svelte";
import TrailShareInfo from "./trail_share_info.svelte";
export let trail: Trail;
export let mode: "overview" | "map" = "map";
@@ -93,7 +94,7 @@
shadowUrl: "",
},
})
.on("loaded", function (e) {
.on("loaded", function (e) {
map.fitBounds(e.target.getBounds());
})
.addTo(map);
@@ -152,14 +153,31 @@
</script>
<div
class="trail-info-panel max-w-5xl mx-auto border border-input-border rounded-3xl overflow-x-hidden h-full"
class="trail-info-panel max-w-5xl mx-auto border border-input-border rounded-3xl h-full"
>
<div class="trail-info-panel-header sticky -top-44 z-10">
<section class="relative h-80">
<img class="w-full h-80" src={thumbnail} alt="" />
<img class="w-full h-80 rounded-3xl" src={thumbnail} alt="" />
<div
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
></div>
{#if trail.public || trail.expand?.trail_share_via_trail?.length}
<div
class="flex absolute top-8 right-10 w-8 h-8 rounded-full items-center justify-center bg-white text-primary"
>
{#if trail.public}
<span
class="tooltip text-2xl"
data-title={$_("public")}
>
<i class="fa fa-globe"></i>
</span>
{/if}
{#if trail.expand?.trail_share_via_trail?.length}
<TrailShareInfo {trail} large={true}></TrailShareInfo>
{/if}
</div>
{/if}
<div
class="flex absolute justify-between items-end w-full bottom-8 left-0 px-8 gap-y-4"
>
@@ -193,7 +211,7 @@
</h3>
</div>
</div>
{#if $currentUser && $currentUser.id == trail.author}
{#if ($currentUser && $currentUser.id == trail.author) || trail.expand.trail_share_via_trail?.length}
<TrailDropdown {trail} {mode}></TrailDropdown>
{/if}
</div>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { page } from "$app/stores";
import type { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util";
import {
@@ -8,6 +7,7 @@
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import TrailShareInfo from "./trail_share_info.svelte";
export let trail: Trail;
@@ -23,20 +23,26 @@
<img class="h-28 w-28 object-cover rounded-xl" src={thumbnail} alt="" />
</div>
<div class="min-w-0 basis-full">
<h4 class="font-semibold text-lg">
{trail.name}{#if trail.public}
<div class="flex items-center gap-4">
<h4 class="font-semibold text-lg">
{trail.name}
</h4>
{#if trail.public}
<span class="tooltip ml-3" data-title={$_("public")}>
<i class="fa fa-globe"></i>
</span>
{/if}
</h4>
{#if trail.expand?.trail_share_via_trail?.length}
<TrailShareInfo {trail}></TrailShareInfo>
{/if}
</div>
{#if trail.date}
<p class="text-xs text-gray-500 mb-3">
{new Date(trail.date).toLocaleDateString(undefined, {
month: "long",
day: "2-digit",
year: "numeric",
timeZone: 'UTC'
timeZone: "UTC",
})}
</p>
{/if}
@@ -54,12 +60,12 @@
<div class="flex mt-1 gap-4 text-sm text-gray-500">
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance
trail.distance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
trail.elevation_gain
trail.elevation_gain,
)}</span
>
<span

View File

@@ -0,0 +1,125 @@
<script lang="ts">
import type { Trail } from "$lib/models/trail";
import type { User } from "$lib/models/user";
import { pb } from "$lib/pocketbase";
import { users_show } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n";
import { fly, slide } from "svelte/transition";
export let trail: Trail;
export let large: boolean = false;
let showInfo: boolean = false;
let loading: boolean = false;
let infoLoaded: boolean = false;
let trailIsOwned: boolean = trail.author == pb.authStore.model?.id;
let author: User;
async function fetchInfo() {
if (!infoLoaded) {
loading = true;
loading = false;
if (trailIsOwned) {
for (const share of trail.expand.trail_share_via_trail ?? []) {
share.expand = {
user: await users_show(share.user),
};
}
} else {
author = await users_show(trail.author!);
}
infoLoaded = true;
}
showInfo = true;
}
</script>
<div class="relative trail-share-info">
<i
class="fa fa-share-nodes"
class:text-xl={large}
role="button"
tabindex="0"
on:mouseenter={fetchInfo}
on:mouseleave={() => (showInfo = false)}
></i>
{#if showInfo}
<ul
class="menu absolute top-7 bg-menu-background border border-input-border rounded-xl shadow-md overflow-hidden p-3 space-y-3 text-content"
in:fly={{y: -10, duration: 150}} out:fly={{y: -10, duration: 150}}
>
{#if loading}
<div class="spinner spinner-dark"></div>
{:else if infoLoaded && trail.expand.trail_share_via_trail}
{#if trailIsOwned}
{#each trail.expand.trail_share_via_trail as share}
{#if share.expand}
<li>
<div class="flex items-center mr-8">
<img
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(
share.expand.user,
share.expand.user.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.user.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
<p class="font-semibold text-base">
{share.expand.user.username}
</p>
<span class="mx-2 text-sm text-gray-500"
>{$_("can")}</span
>
<p class="whitespace-nowrap text-base">
{#if share.permission == "view"}
<i class="fa fa-eye"></i>
{:else}
<i class="fa fa-pen"></i>
{/if}
</p>
</div>
</li>
{/if}
{/each}
{:else if author}
<li>
<p class="text-xs text-gray-500 mb-2">
{$_("shared-by")}
</p>
<div class="flex items-center mr-8">
<img
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(author, author.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
<p class="font-semibold">
{author.username}
</p>
</div>
<p class="text-xs text-gray-500 mt-2">
{$_("you-can")}
</p>
<p class="whitespace-nowrap">
{#if trail.expand.trail_share_via_trail[0].permission == "view"}
<i class="fa fa-eye mr-1"></i>
{$_("view")}
{:else}
<i class="fa fa-pen mr-1"></i>
{$_("edit")}
{/if}
</p>
</li>
{/if}
{/if}
</ul>
{/if}
</div>

View File

@@ -1,15 +1,35 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import type { Trail } from "$lib/models/trail";
import { TrailShare } from "$lib/models/trail_share";
import type { User } from "$lib/models/user";
import { show_toast } from "$lib/stores/toast_store";
import {
shares,
trail_share_create,
trail_share_delete,
trail_share_index,
trail_share_update,
} 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 { users_search } from "$lib/stores/user_store";
import type { User } from "$lib/models/user";
import { show_toast } from "$lib/stores/toast_store";
import type { SelectItem } from "../base/select.svelte";
import Select from "../base/select.svelte";
export let openModal: (() => void) | undefined = undefined;
let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
export function openShareModal() {
fetchShares();
if (openModal) {
openModal();
}
}
export let trail: Trail;
const dispatch = createEventDispatcher();
@@ -17,14 +37,21 @@
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.id,
icon: "user"
}))
value: u,
icon: "user",
}));
} catch (e) {
console.error(e);
show_toast({
@@ -47,21 +74,90 @@
dispatch("save");
closeModal!();
}
async function shareTrail(item: SelectItem) {
const share = new TrailShare(item.value.id, trail.id!, "view");
await trail_share_create(share);
fetchShares();
}
async function updateSharePermission(share:TrailShare, permission: "view"| "edit" ) {
share.permission = permission;
await trail_share_update(share);
}
async function deleteShare(share: TrailShare) {
await trail_share_delete(share);
fetchShares();
}
async function fetchShares() {
sharesLoading = true;
await trail_share_index(trail.id!);
sharesLoading = false;
}
</script>
<Modal
id="share-modal"
title="Share this trail"
size="max-w-lg"
title={$_('share-this-trail')}
size="max-w-sm overflow-visible"
bind:openModal
bind:closeModal
>
<div slot="content">
<Search
on:update={(e) => updateUsers(e.detail)}
on:click={(e) => shareTrail(e.detail)}
placeholder={`${$_("username")}`}
items={searchItems}
></Search>
>
<img
slot="item-header"
let:item
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(item.value, item.value.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${item.value.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
</Search>
<h4 class="font-semibold mt-4">{$_('shared-with')}</h4>
{#if $shares.length == 0}
<p class="text-gray-500 text-center mt-2 text-sm">
{$_('trail-not-shared')}
</p>
{:else}
{#each $shares as share}
{#if share.expand}
<div class="flex items-center gap-x-2 p-2">
<img
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(
share.expand.user,
share.expand.user.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.user.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
<p>{share.expand.user.username}</p>
<span class="basis-full text-sm text-center text-gray-500">{$_('can')}</span>
<div class="shrink-0">
<Select
bind:value={share.permission}
items={permissionSelectItems}
on:change={(e) => updateSharePermission(share, e.detail)}
></Select>
</div>
<button class="btn-icon text-red-500"
on:click={() => deleteShare(share)}
><i class="fa fa-trash"></i></button
>
</div>
{/if}
{/each}
{/if}
</div>
<div slot="footer" class="flex justify-between items-center gap-4">
<Button

View File

@@ -11,6 +11,7 @@
"api-documentation": "API Dokumentation",
"avatar": "Avatar",
"basic-info": "Basisinformation",
"can": "kann",
"cancel": "Abbrechen",
"card": "{n, plural, =1 {Karte} other {Karten}}",
"categories": "Kategorien",
@@ -23,7 +24,7 @@
"completed": "Abgeschlossen",
"completion-status": "Abschlussstatus",
"contribute": "Mitwirken",
"copy-link": "",
"copy-link": "Link kopieren",
"create-new-list": "Neue Liste erstellen",
"creation-date": "Erstellungsdatum",
"cycling": "Radfahren",
@@ -133,7 +134,10 @@
"search-trails": "Route suchen",
"select-list": "Liste auswählen",
"settings": "Einstellungen",
"share": "",
"share": "Teilen",
"share-this-trail": "Diese Route teilen",
"shared-by": "Geteilt von",
"shared-with": "Geteilt mit",
"show-in-overview": "In der Übersicht anzeigen",
"show-on-map": "Auf der Karte anzeigen",
"slogan": "Speichere deine Abenteuer!",
@@ -142,12 +146,15 @@
"summit-book": "Gipfelbuch",
"text": "Text",
"trail": "{n, plural, =1 {Route} other {Routen}}",
"trail-not-shared": "Route mit niemandem geteilt",
"trails-for-you": "Routen für dich",
"units": "Einheiten",
"upload-file": "Datei hochladen",
"upload-gpx": "GPX hochladen",
"username": "Nutzername",
"view": "Ansehen",
"waypoints": "{n, plural, =1 {Wegpunkt} other {Wegpunkte}}",
"welcome_to": "Willkommen bei",
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort"
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort",
"you-can": "Sie können"
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "API Documentation",
"avatar": "Avatar",
"basic-info": "Basic Info",
"can": "can",
"cancel": "Cancel",
"card": "{n, plural, =1 {Card} other {Cards}}",
"categories": "Categories",
@@ -134,6 +135,9 @@
"select-list": "Select List",
"settings": "Settings",
"share": "Share",
"share-this-trail": "Share this trail",
"shared-by": "Shared by",
"shared-with": "Shared with",
"show-in-overview": "Show in overview",
"show-on-map": "Show on map",
"slogan": "Save your adventures!",
@@ -142,12 +146,15 @@
"summit-book": "Summit Book",
"text": "Text",
"trail": "{n, plural, =1 {Trail} other {Trails}}",
"trail-not-shared": "Not shared with anyone",
"trails-for-you": "Trails for you",
"units": "Units",
"upload-file": "Upload file",
"upload-gpx": "Upload GPX",
"username": "Username",
"view": "View",
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
"welcome_to": "Welcome to",
"wrong-username-or-password": "Wrong username or password"
"wrong-username-or-password": "Wrong username or password",
"you-can": "You can"
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "Documentation API",
"avatar": "Avatar",
"basic-info": "Informations de base",
"can": "",
"cancel": "Annuler",
"card": "{n, plural, =1 {Carte} other {Cartes}}",
"categories": "Catégories",
@@ -134,6 +135,9 @@
"select-list": "Liste de choix",
"settings": "Paramètres",
"share": "",
"share-this-trail": "",
"shared-by": "",
"shared-with": "",
"show-in-overview": "Voir dans l'aperçu",
"show-on-map": "Voir sur la carte",
"slogan": "Sauvegarder vos aventures !",
@@ -142,12 +146,15 @@
"summit-book": "Livre du sommet",
"text": "Texte",
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
"trail-not-shared": "",
"trails-for-you": "Itinéraire pour vous",
"units": "Unités",
"upload-file": "Téléverser un fichier",
"upload-gpx": "Envoyer un GPX",
"username": "Nom d'utilisteur",
"view": "",
"waypoints": "Point de repère",
"welcome_to": "Bienvenue à",
"wrong-username-or-password": "Nom d'utilisateur ou mot de passe incorrect"
"wrong-username-or-password": "Nom d'utilisateur ou mot de passe incorrect",
"you-can": ""
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "API Dokumentáció",
"avatar": "Avatar",
"basic-info": "Alap információk",
"can": "",
"cancel": "Mégsem",
"card": "{n, plural, =1 {Kártya} other {Kártyák}}",
"categories": "Kategóriák",
@@ -134,6 +135,9 @@
"select-list": "Lista kiválasztása",
"settings": "Beállítások",
"share": "",
"share-this-trail": "",
"shared-by": "",
"shared-with": "",
"show-in-overview": "Áttekintés",
"show-on-map": "Mutatás térképen",
"slogan": "Mentse el a kalandjait!",
@@ -142,12 +146,15 @@
"summit-book": "Csúcspont könyv",
"text": "Szöveg",
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
"trail-not-shared": "",
"trails-for-you": "Útvonalak önnek",
"units": "Mértékegységek",
"upload-file": "Fájl feltöltése",
"upload-gpx": "GPX feltöltése",
"username": "Felhasználónév",
"view": "",
"waypoints": "Útvonalpontok",
"welcome_to": "Üdvözöljük a",
"wrong-username-or-password": "Helytelen felhasználónév vagy jelszó"
"wrong-username-or-password": "Helytelen felhasználónév vagy jelszó",
"you-can": ""
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "API-documentatie",
"avatar": "Profielfoto",
"basic-info": "Algemene informatie",
"can": "",
"cancel": "Annuleren",
"card": "{n, plural, =1 {kaart} other {kaarten}}",
"categories": "Categorieën",
@@ -134,6 +135,9 @@
"select-list": "Kies een lijst",
"settings": "Instellingen",
"share": "",
"share-this-trail": "",
"shared-by": "",
"shared-with": "",
"show-in-overview": "Tonen op overzicht",
"show-on-map": "Tonen op kaart",
"slogan": "Bewaar je avonturen!",
@@ -142,12 +146,15 @@
"summit-book": "Bergtopboek",
"text": "Tekst",
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",
"trail-not-shared": "",
"trails-for-you": "Wandelroutes voor jou",
"units": "Eenheden",
"upload-file": "Bestand uploaden",
"upload-gpx": "GPX-bestand uploaden",
"username": "Gebruikersnaam",
"view": "",
"waypoints": "Routepunten",
"welcome_to": "Welkom bij",
"wrong-username-or-password": "Onjuiste gebruikersnaam of wachtwoord"
"wrong-username-or-password": "Onjuiste gebruikersnaam of wachtwoord",
"you-can": ""
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "Dokumentacja API",
"avatar": "Awatar",
"basic-info": "Podstawowe informacje",
"can": "",
"cancel": "Anuluj",
"card": "{n, plural, =1 {Karta} other {Karty}}",
"categories": "Kategorie",
@@ -134,6 +135,9 @@
"select-list": "Wybierz Listę",
"settings": "Ustawienia",
"share": "",
"share-this-trail": "",
"shared-by": "",
"shared-with": "",
"show-in-overview": "Pokaż w przeglądzie",
"show-on-map": "Pokaż na mapie",
"slogan": "Zapisz swoją wyprawę!",
@@ -142,12 +146,15 @@
"summit-book": "Logbook",
"text": "Tekst",
"trail": "{n, plural, =1 {Szlak} other {Szlaki}}",
"trail-not-shared": "",
"trails-for-you": "Ścieżki dla ciebie",
"units": "Jednostki",
"upload-file": "Przesyłanie pliku",
"upload-gpx": "Importuj GPX",
"username": "Nazwa Użytkownika",
"view": "",
"waypoints": "Punkty",
"welcome_to": "Witaj w",
"wrong-username-or-password": "Zła nazwa użytkownika lub hasło"
"wrong-username-or-password": "Zła nazwa użytkownika lub hasło",
"you-can": ""
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "Documentação da API",
"avatar": "Avatar",
"basic-info": "Informações básicas",
"can": "",
"cancel": "Cancelar",
"card": "{n, plural, =1 {Cartão} other {cartãos}}",
"categories": "Categorias",
@@ -134,6 +135,9 @@
"select-list": "Selecionar lista",
"settings": "Definições",
"share": "",
"share-this-trail": "",
"shared-by": "",
"shared-with": "",
"show-in-overview": "Mostrar na vista geral",
"show-on-map": "Mostrar no mapa",
"slogan": "Guarde as suas aventuras!",
@@ -142,12 +146,15 @@
"summit-book": "Livro da cimeira",
"text": "Texto",
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
"trail-not-shared": "",
"trails-for-you": "Trilhos para si",
"units": "Unidades",
"upload-file": "Subir arquivo",
"upload-gpx": "Carregar GPX",
"username": "Nome de utilizador",
"view": "",
"waypoints": "Pontos de passagem",
"welcome_to": "Bem-vindo ao",
"wrong-username-or-password": "Nome de utilizador ou palavra-passe errados"
"wrong-username-or-password": "Nome de utilizador ou palavra-passe errados",
"you-can": ""
}

View File

@@ -11,6 +11,7 @@
"api-documentation": "API 文档",
"avatar": "头像",
"basic-info": "基本信息",
"can": "",
"cancel": "取消",
"card": "{n, plural, =1 {卡片} other {卡片}}",
"categories": "分类",
@@ -134,6 +135,9 @@
"select-list": "选择列表",
"settings": "设置",
"share": "",
"share-this-trail": "",
"shared-by": "",
"shared-with": "",
"show-in-overview": "详情中展示",
"show-on-map": "地图中展示",
"slogan": "保存你的冒险!",
@@ -142,12 +146,15 @@
"summit-book": "详细日程",
"text": "文本",
"trail": "{n, plural, =1 {路线} other {路线}}",
"trail-not-shared": "",
"trails-for-you": "推荐路线",
"units": "单位",
"upload-file": "上传文件",
"upload-gpx": "上传 GPX",
"username": "用户名",
"view": "",
"waypoints": "坐标",
"welcome_to": "欢迎",
"wrong-username-or-password": "用户名或密码无效"
"wrong-username-or-password": "用户名或密码无效",
"you-can": ""
}

View File

@@ -1,6 +1,7 @@
import type { Category } from "./category";
import type { Comment } from "./comment";
import type { SummitLog } from "./summit_log";
import type { TrailShare } from "./trail_share";
import type { Waypoint } from "./waypoint";
class Trail {
@@ -28,6 +29,7 @@ class Trail {
summit_logs: SummitLog[]
comments_via_trail: Comment[]
gpx_data?: string
trail_share_via_trail?: TrailShare[]
}
tags?: string[];
description?: string;
@@ -52,6 +54,7 @@ class Trail {
waypoints?: Waypoint[],
summit_logs?: SummitLog[],
comments?: Comment[],
shares?: TrailShare[],
tags?: string[],
description?: string
created?: string
@@ -78,7 +81,8 @@ class Trail {
category: params?.category,
waypoints: params?.waypoints ?? [],
summit_logs: params?.summit_logs ?? [],
comments_via_trail: params?.comments ?? []
comments_via_trail: params?.comments ?? [],
trail_share_via_trail: params?.shares ?? []
}
this.tags = params?.tags ?? []
this.description = params?.description ?? "";

View File

@@ -0,0 +1,17 @@
import type { User } from "./user";
export class TrailShare {
id?: string;
user: string;
trail: string;
permission: "view" | "edit"
expand?: {
user: User
}
constructor(user: string, trail: string, permission: "view" | "edit") {
this.user = user;
this.trail = trail;
this.permission = permission
}
}

View File

@@ -0,0 +1,57 @@
import type { TrailShare } from "$lib/models/trail_share";
import { ClientResponseError } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const shares: Writable<TrailShare[]> = writable([])
export async function trail_share_index(trail: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/trail-share?' + new URLSearchParams({
filter: `trail='${trail}'`,
}), {
method: 'GET',
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const response: TrailShare[] = await r.json();
shares.set(response);
return shares;
}
export async function trail_share_create(share: TrailShare) {
let r = await fetch('/api/v1/trail-share', {
method: 'PUT',
body: JSON.stringify(share),
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
}
export async function trail_share_update(share: TrailShare) {
let r = await fetch('/api/v1/trail-share/' + share.id, {
method: 'POST',
body: JSON.stringify(share),
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
}
export async function trail_share_delete(share: TrailShare) {
const r = await fetch('/api/v1/trail-share/' + share.id, {
method: 'DELETE',
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
}

View File

@@ -55,7 +55,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
}
r = await f('/api/v1/trail?' + new URLSearchParams({
expand: "category,waypoints,summit_logs",
expand: "category,waypoints,summit_logs,trail_share_via_trail",
filter: trailIds.map((id: Record<string, any>) => `id="${id}"`).join('||'),
sort: `${filter.sortOrder}${filter.sort}`
}), {
@@ -131,7 +131,7 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
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({
expand: "category,waypoints,summit_logs,comments_via_trail.author",
expand: "category,waypoints,summit_logs,comments_via_trail.author,trail_share_via_trail",
}), {
method: 'GET',
})

View File

@@ -24,8 +24,8 @@ export async function users_create(user: User) {
}
export async function users_search(q: string) {
let r = await fetch('/api/v1/user?' + new URLSearchParams({
"filter": `username~"${q}"`,
let r = await fetch('/api/v1/user/anonymous?' + new URLSearchParams({
"filter": `username~"${q}"&&id!="${pb.authStore.model?.id}"`,
}), {
method: 'GET',
})
@@ -38,6 +38,19 @@ export async function users_search(q: string) {
}
}
export async function users_show(id: string) {
let r = await fetch(`/api/v1/user/anonymous/${id}`, {
method: 'GET',
})
const response = await r.json()
if (r.ok) {
return response;
} else {
throw new ClientResponseError(response)
}
}
export async function users_auth_methods(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<AuthMethodsList> {
const r = await f('/api/v1/auth/oauth', {
method: 'GET',