adds trail share

This commit is contained in:
Christian Beutel
2024-09-13 20:06:46 +02:00
parent 421191ec77
commit c38d415460
34 changed files with 1144 additions and 104 deletions

View File

@@ -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")}</button
>
<button class="btn-danger" type="button" on:click={confirm} name="delete"
>{$_("delete")}</button
>{$_(action)}</button
>
</div></Modal
>

View File

@@ -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" }]
: []),
];
</script>
<div
@@ -50,11 +63,18 @@
</div>
{/if}
<div class="self-start min-w-0 w-full transition-transform">
<div class="flex justify-between items-center">
<h5 class="text-xl font-semibold overflow-hidden overflow-ellipsis">
<div class="flex items-center gap-3">
<h5
class="text-xl font-semibold overflow-hidden overflow-ellipsis basis-full"
>
{list.name}
</h5>
<Dropdown items={dropdownItems} on:change></Dropdown>
{#if listIsShared}
<ShareInfo type="list" subject={list}></ShareInfo>
{/if}
{#if dropdownItems.length}
<Dropdown items={dropdownItems} on:change></Dropdown>
{/if}
</div>
<div class="flex mt-1 gap-4 text-sm text-gray-500 whitespace-nowrap">
<span
@@ -73,12 +93,12 @@
)}</span
>
</div>
<p class="text-sm text-gray-500 mb-2"
>{list.expand?.trails.length ?? 0}
<p class="text-sm text-gray-500 mb-2">
{list.expand?.trails.length ?? 0}
{$_("trail", {
values: { n: list.expand?.trails.length ?? 0 },
})}</p
>
})}
</p>
<p
class="text-gray-500 text-sm mr-8 whitespace-pre-wrap {active
? ''

View File

@@ -0,0 +1,222 @@
<script lang="ts">
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;
}
</script>
<Modal
id="share-modal"
title={$_("share-this-list")}
size="max-w-sm overflow-visible"
bind:openModal
bind:closeModal
>
<div slot="content">
<p class="p-4 bg-amber-100 rounded-xl mb-4 text-sm text-gray-500">
{$_("list-share-warning")}
</p>
<Search
on:update={(e) => updateUsers(e.detail)}
on:click={(e) => shareList(e.detail)}
placeholder={`${$_("username")}`}
items={searchItems}
>
<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">
{$_("list-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
secondary={true}
disabled={copyButtonText == $_("link-copied")}
on:click={copyURLToClipboard}
>
<i class="fa fa-link mr-2"></i>
{copyButtonText}
</Button>
<button class="btn-primary" on:click={close}>{$_("close")}</button>
</div></Modal
>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import type { List } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import type { User } from "$lib/models/user";
import { pb } from "$lib/pocketbase";
@@ -7,31 +8,30 @@
import { _ } from "svelte-i18n";
import { fly, slide } from "svelte/transition";
export let trail: Trail;
export let subject: Trail | List;
export let large: boolean = false;
export let type: "trail" | "list"
const shareData = type == "trail" ? (subject as Trail).expand.trail_share_via_trail : (subject as List).expand?.list_share_via_list
let showInfo: boolean = false;
let loading: boolean = false;
let infoLoaded: boolean = false;
let trailIsOwned: boolean = trail.author == pb.authStore.model?.id;
let subjectIsOwned: boolean = subject.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 ?? []) {
if (subjectIsOwned) {
for (const share of shareData ?? []) {
share.expand = {
user: await users_show(share.user),
};
}
} else {
author = await users_show(trail.author!);
author = await users_show(subject.author!);
}
infoLoaded = true;
@@ -57,9 +57,9 @@
>
{#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}
{:else if infoLoaded && shareData}
{#if subjectIsOwned}
{#each shareData as share}
{#if share.expand}
<li>
<div class="flex items-center mr-8">
@@ -109,7 +109,7 @@
{$_("you-can")}
</p>
<p class="whitespace-nowrap">
{#if trail.expand.trail_share_via_trail[0].permission == "view"}
{#if shareData[0].permission == "view"}
<i class="fa fa-eye mr-1"></i>
{$_("view")}
{:else}

View File

@@ -7,7 +7,7 @@
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import TrailShareInfo from "./trail_share_info.svelte";
import ShareInfo from "../share_info.svelte";
export let trail: Trail;
@@ -45,7 +45,7 @@
</span>
{/if}
{#if trail.expand?.trail_share_via_trail?.length}
<TrailShareInfo {trail}></TrailShareInfo>
<ShareInfo type="trail" subject={trail}></ShareInfo>
{/if}
</div>
{/if}

View File

@@ -33,7 +33,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";
import ShareInfo from "../share_info.svelte";
export let trail: Trail;
export let mode: "overview" | "map" = "map";
@@ -190,7 +190,7 @@
</span>
{/if}
{#if trailIsShared}
<TrailShareInfo {trail} large={true}></TrailShareInfo>
<ShareInfo type="trail" subject={trail} large={true}></ShareInfo>
{/if}
</div>
{/if}

View File

@@ -7,7 +7,7 @@
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import TrailShareInfo from "./trail_share_info.svelte";
import ShareInfo from "../share_info.svelte";
export let trail: Trail;
@@ -33,7 +33,7 @@
</span>
{/if}
{#if trail.expand?.trail_share_via_trail?.length}
<TrailShareInfo {trail}></TrailShareInfo>
<ShareInfo type="trail" subject={trail}></ShareInfo>
{/if}
</div>
{#if trail.date}

View File

@@ -93,7 +93,7 @@
async function fetchShares() {
sharesLoading = true;
await trail_share_index(trail.id!);
await trail_share_index({trail: trail.id!});
sharesLoading = false;
}
</script>