adds trail share
This commit is contained in:
36
web/src/routes/api/v1/list-share/+server.ts
Normal file
36
web/src/routes/api/v1/list-share/+server.ts
Normal file
@@ -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<ListShare>({
|
||||
sort: sort,
|
||||
filter: filter
|
||||
})
|
||||
for (const share of r) {
|
||||
const anonymous_user = await pb.collection('users_anonymous').getOne<User>(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<ListShare>(data)
|
||||
return json(r);
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e)
|
||||
}
|
||||
}
|
||||
33
web/src/routes/api/v1/list-share/[id]/+server.ts
Normal file
33
web/src/routes/api/v1/list-share/[id]/+server.ts
Normal file
@@ -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<ListShare>(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<ListShare>(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)
|
||||
}
|
||||
}
|
||||
@@ -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<List>({
|
||||
expand: "trails,trails.waypoints,trails.category",
|
||||
expand: "trails,trails.waypoints,trails.category,list_share_via_list",
|
||||
sort: sort,
|
||||
})
|
||||
return json(r)
|
||||
|
||||
@@ -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<List>(event.params.id as string, { expand: "trails,trails.waypoints,trails.category" })
|
||||
.getOne<List>(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);
|
||||
|
||||
@@ -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 @@
|
||||
<svelte:head>
|
||||
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 mx-4"
|
||||
style="height: calc(100vh - 124px)"
|
||||
>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 mx-4">
|
||||
<ul
|
||||
class="list-list mx-4 md:mx-auto rounded-xl border border-input-border max-w-full overflow-y-scroll"
|
||||
class="list-list mx-4 md:mx-auto rounded-xl border border-input-border max-h-full"
|
||||
>
|
||||
<div
|
||||
class="flex gap-x-4 items-center justify-between p-4 top-0 sticky bg-background z-50"
|
||||
class="flex gap-x-4 items-center justify-between p-4 bg-background z-50 rounded-xl"
|
||||
>
|
||||
<a
|
||||
class="btn-primary btn-large text-center mx-4"
|
||||
href="/lists/edit/new"
|
||||
<a class="btn-primary btn-large text-center" href="/lists/edit/new"
|
||||
><i class="fa fa-plus mr-2"></i>{$_("new-list")}</a
|
||||
>
|
||||
<button type="button" class="btn-icon" on:click={toggleMap}
|
||||
@@ -113,24 +114,24 @@
|
||||
</div>
|
||||
|
||||
<hr class="border-separator mb-2" />
|
||||
{#each $lists as item, i}
|
||||
<li
|
||||
class="list-list-item"
|
||||
on:click={() => setCurrentList(item)}
|
||||
role="presentation"
|
||||
>
|
||||
<ListCard
|
||||
list={item}
|
||||
on:change={(e) => handleDropdownClick(e, item)}
|
||||
active={item.id === $list.id}
|
||||
></ListCard>
|
||||
{#if i != $lists.length - 1}
|
||||
<hr class="border-separator my-2" />
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
<div class="px-4">
|
||||
{#each $lists as item, i}
|
||||
<li
|
||||
class="list-list-item my-1"
|
||||
on:click={() => setCurrentList(item)}
|
||||
role="presentation"
|
||||
>
|
||||
<ListCard
|
||||
list={item}
|
||||
on:change={(e) => handleDropdownClick(e, item)}
|
||||
active={item.id === $list.id}
|
||||
></ListCard>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</ul>
|
||||
<div class:hidden={!showMap}>
|
||||
<div id="trail-map" class="sticky top-[62px]" class:hidden={!showMap}>
|
||||
<MapWithElevationMultiple
|
||||
trails={$list.expand?.trails ?? []}
|
||||
bind:map
|
||||
@@ -151,10 +152,13 @@
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteList}
|
||||
></ConfirmModal>
|
||||
<ListShareModal list={$list} bind:openShareModal></ListShareModal>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#map {
|
||||
min-height: 600px;
|
||||
@media only screen and (min-width: 768px) {
|
||||
#trail-map {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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<List>({
|
||||
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<string, any>) => ({
|
||||
searchDropdownItems = response.results[0].hits
|
||||
.filter((h: List) => !$form.trails?.includes(h.id!))
|
||||
.map((t: Record<string, any>) => ({
|
||||
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"),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title
|
||||
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
|
||||
<form
|
||||
id="list-form"
|
||||
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0"
|
||||
class="flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0 overflow-y-scroll"
|
||||
style="max-height: calc(100vh - 124px)"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<h2 class="text-2xl font-semibold">
|
||||
@@ -248,10 +318,28 @@
|
||||
{loading}>{$_("save-list")}</Button
|
||||
>
|
||||
</form>
|
||||
<MapWithElevationMultiple
|
||||
trails={$form.expand?.trails ?? []}
|
||||
options={{flyToBounds: true}}
|
||||
bind:activeTrailIndex
|
||||
bind:this={map}
|
||||
></MapWithElevationMultiple>
|
||||
<div id="trail-map" class="max-h-full">
|
||||
<MapWithElevationMultiple
|
||||
trails={$form.expand?.trails ?? []}
|
||||
options={{ flyToBounds: true }}
|
||||
bind:activeTrailIndex
|
||||
bind:this={map}
|
||||
></MapWithElevationMultiple>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ConfirmModal
|
||||
text={$_('list-share-warning-update')}
|
||||
title={$_("confirm-share")}
|
||||
action="confirm"
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={updateTrailShares}
|
||||
></ConfirmModal>
|
||||
|
||||
<style>
|
||||
@media only screen and (min-width: 768px) {
|
||||
#trail-map {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||
<div id="trail-details" class=" sticky top-[62px]">
|
||||
<div id="trail-details" class="sticky top-[62px]">
|
||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user