adds lists search and filter

This commit is contained in:
Christian Beutel
2024-12-13 18:05:57 +01:00
parent 673eb78dd0
commit c00cf85fed
20 changed files with 417 additions and 104 deletions

View File

@@ -1,5 +1,10 @@
<script lang="ts">
export let fullWidth: boolean = false;
</script>
<div <div
class="skeleton-card relative rounded-2xl sm:w-72 animate-pulse flex flex-col w-80 p-4 bg-menu-background border border-input-border" class="skeleton-card relative rounded-2xl animate-pulse flex flex-col {fullWidth ? "w-full" : "sm:w-72 w-80"} p-4 bg-menu-background border border-input-border"
aria-hidden="true" aria-hidden="true"
> >
<!-- Image placeholder --> <!-- Image placeholder -->

View File

@@ -25,7 +25,7 @@
<button class="btn-secondary" on:click={closeModal} <button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button >{$_("cancel")}</button
> >
<button class={action === "delete" ? "btn-delete" : "btn-primary"} type="button" on:click={confirm} name="delete" <button class={action === "delete" ? "btn-danger" : "btn-primary"} type="button" on:click={confirm} name="delete"
>{$_(action)}</button >{$_(action)}</button
> >
</div></Modal </div></Modal

View File

@@ -51,18 +51,15 @@
<i class="fa fa-table-list text-5xl"></i> <i class="fa fa-table-list text-5xl"></i>
</div> </div>
{/if} {/if}
<div class="self-start min-w-0 w-full transition-transform"> <div class="self-start min-w-0 basis-full transition-transform">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h5 <h5
class="text-xl font-semibold overflow-hidden overflow-ellipsis basis-full" class="text-xl font-semibold overflow-hidden overflow-ellipsis"
> >
{list.name} {list.name}
</h5> </h5>
{#if list.public} {#if list.public}
<span <span class="tooltip" data-title={$_("public")}>
class="tooltip"
data-title={$_("public")}
>
<i class="fa fa-globe"></i> <i class="fa fa-globe"></i>
</span> </span>
{/if} {/if}
@@ -70,6 +67,21 @@
<ShareInfo type="list" subject={list}></ShareInfo> <ShareInfo type="list" subject={list}></ShareInfo>
{/if} {/if}
</div> </div>
{#if list.expand?.author}
<p class="text-xs text-gray-500 my-2">
{$_("by")}
<img
class="rounded-full w-5 aspect-square mx-1 inline"
src={getFileURL(
list.expand.author,
list.expand.author.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
{list.expand?.author.username}
</p>
{/if}
<div <div
class="grid grid-cols-2 mt-1 mb-2 gap-x-4 gap-y-1 text-sm text-gray-500 whitespace-nowrap flex-wrap" class="grid grid-cols-2 mt-1 mb-2 gap-x-4 gap-y-1 text-sm text-gray-500 whitespace-nowrap flex-wrap"
> >

View File

@@ -42,8 +42,12 @@
list.author == $currentUser?.id || list.author == $currentUser?.id ||
list.expand?.list_share_via_list?.some((s) => s.permission == "edit"); list.expand?.list_share_via_list?.some((s) => s.permission == "edit");
$: allowShare =
list.author == $currentUser?.id &&
!list.expand?.trails?.some((t) => t.author !== $currentUser?.id);
$: dropdownItems = [ $: dropdownItems = [
...(list.author == $currentUser?.id ...(allowShare
? [{ text: $_("share"), value: "share", icon: "share" }] ? [{ text: $_("share"), value: "share", icon: "share" }]
: []), : []),
...(allowEdit ...(allowEdit
@@ -122,7 +126,23 @@
<div class="p-4 md:p-6"> <div class="p-4 md:p-6">
<h4 class="text-2xl font-semibold mb-4">{list.name}</h4> <h4 class="text-2xl font-semibold mb-4">{list.name}</h4>
{#if list.expand?.author}
<p class="my-3 text-gray-500 text-sm">
{$_("by")}
<img
class="rounded-full w-8 aspect-square mx-1 inline"
src={getFileURL(
list.expand.author,
list.expand.author.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
<a class="underline" href="/profile/{list.expand.author.id}"
>{list.expand.author.username}</a
>
</p>
{/if}
<hr /> <hr />
<div <div
class="grid grid-cols-2 my-4 gap-4 font-semibold whitespace-nowrap flex-wrap justify-around" class="grid grid-cols-2 my-4 gap-4 font-semibold whitespace-nowrap flex-wrap justify-around"

View File

@@ -2,11 +2,12 @@
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { List } from "$lib/models/list"; import type { List } from "$lib/models/list";
import { lists } from "$lib/stores/list_store";
import { trail } from "$lib/stores/trail_store"; import { trail } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import Modal from "../base/modal.svelte"; import Modal from "../base/modal.svelte";
import { getFileURL } from "$lib/util/file_util";
export let lists: List[];
export let openModal: (() => void) | undefined = undefined; export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined; export let closeModal: (() => void) | undefined = undefined;
@@ -34,7 +35,7 @@
<slot {openModal} /> <slot {openModal} />
<ul slot="content"> <ul slot="content">
{#each $lists as list} {#each lists as list}
<li <li
class="flex gap-4 items-center p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer" class="flex gap-4 items-center p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
on:click={() => handleSelect(list)} on:click={() => handleSelect(list)}

View File

@@ -3,7 +3,6 @@
import type { List } from "$lib/models/list"; import type { List } from "$lib/models/list";
import { ListShare } from "$lib/models/list_share"; import { ListShare } from "$lib/models/list_share";
import { TrailShare } from "$lib/models/trail_share"; import { TrailShare } from "$lib/models/trail_share";
import type { User } from "$lib/models/user";
import { import {
list_share_create, list_share_create,
list_share_delete, list_share_delete,
@@ -11,19 +10,15 @@
list_share_update, list_share_update,
shares, shares,
} from "$lib/stores/list_share_store"; } from "$lib/stores/list_share_store";
import { lists } from "$lib/stores/list_store";
import { show_toast } from "$lib/stores/toast_store";
import { import {
trail_share_create, trail_share_create,
trail_share_delete, trail_share_delete,
trail_share_index, trail_share_index,
} from "$lib/stores/trail_share_store"; } from "$lib/stores/trail_share_store";
import { users_search } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import Button from "../base/button.svelte"; import Button from "../base/button.svelte";
import Search, { type SearchItem } from "../base/search.svelte";
import type { SelectItem } from "../base/select.svelte"; import type { SelectItem } from "../base/select.svelte";
import Select from "../base/select.svelte"; import Select from "../base/select.svelte";
import UserSearch from "../user_search.svelte"; import UserSearch from "../user_search.svelte";
@@ -116,8 +111,8 @@
trails: list.expand?.trails ?? [], trails: list.expand?.trails ?? [],
list_share_via_list: fetchedShares, list_share_via_list: fetchedShares,
}; };
lists.set($lists); sharesLoading = false;
sharesLoading = false; dispatch("update", list)
} }
</script> </script>

View File

@@ -52,7 +52,7 @@
></i> ></i>
{#if showInfo} {#if showInfo}
<ul <ul
class="menu absolute z-10 top-8 bg-menu-background border border-input-border rounded-xl shadow-md overflow-hidden p-3 space-y-3 text-content" class="menu absolute z-10 top-8 bg-menu-background border border-input-border rounded-xl shadow-md overflow-hidden p-3 space-y-3 text-content -translate-x-3/4"
in:fly={{y: -10, duration: 150}} out:fly={{y: -10, duration: 150}} in:fly={{y: -10, duration: 150}} out:fly={{y: -10, duration: 150}}
> >
{#if loading} {#if loading}

View File

@@ -30,6 +30,8 @@
let openExportModal: () => void; let openExportModal: () => void;
let openShareModal: () => void; let openShareModal: () => void;
let lists: List[] = [];
const allowEdit = const allowEdit =
trail.author == $currentUser?.id || trail.author == $currentUser?.id ||
trail.expand.trail_share_via_trail?.some((s) => s.permission == "edit"); trail.expand.trail_share_via_trail?.some((s) => s.permission == "edit");
@@ -76,6 +78,13 @@
: `/trail/view/${trail.id!}`, : `/trail/view/${trail.id!}`,
); );
} else if (item.value == "list") { } else if (item.value == "list") {
lists = (
await lists_index(
{ q: "", author: $currentUser?.id ?? "" },
1,
-1,
)
).items;
openListSelectModal(); openListSelectModal();
} else if (item.value == "direction") { } else if (item.value == "direction") {
window window
@@ -175,7 +184,6 @@
text: `${$_("added-trail-to")} "${list.name}"`, text: `${$_("added-trail-to")} "${list.name}"`,
}); });
} }
await lists_index();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -206,6 +214,7 @@
on:confirm={deleteTrail} on:confirm={deleteTrail}
></ConfirmModal> ></ConfirmModal>
<ListSelectModal <ListSelectModal
{lists}
bind:openModal={openListSelectModal} bind:openModal={openListSelectModal}
on:change={(e) => handleListSelection(e.detail)} on:change={(e) => handleListSelection(e.detail)}
></ListSelectModal> ></ListSelectModal>

View File

@@ -1,6 +1,7 @@
import { object, string } from "yup"; import { object, string } from "yup";
import type { Trail } from "./trail"; import type { Trail } from "./trail";
import type { ListShare } from "./list_share"; import type { ListShare } from "./list_share";
import type { UserAnonymous } from "./user";
export class List { export class List {
id?: string; id?: string;
@@ -12,6 +13,7 @@ export class List {
expand?: { expand?: {
trails?: Trail[] trails?: Trail[]
list_share_via_list?: ListShare[] list_share_via_list?: ListShare[]
author?: UserAnonymous;
} }
author?: string; author?: string;
@@ -29,8 +31,11 @@ export class List {
export interface ListFilter { export interface ListFilter {
q: string, q: string,
sort: "name" | "size" | "created"; sort?: "name" | "size" | "created";
sortOrder: "+" | "-" author?: string;
public?: boolean;
shared?: boolean;
sortOrder?: "+" | "-"
} }
export const listSchema = object<List>({ export const listSchema = object<List>({

View File

@@ -1,18 +1,22 @@
import { List, type ListFilter } from "$lib/models/list"; import { List, type ListFilter } from "$lib/models/list";
import type { Trail } from "$lib/models/trail"; import type { Trail } from "$lib/models/trail";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { getFileURL } from "$lib/util/file_util"; import { ClientResponseError, type ListResult } from "pocketbase";
import { ClientResponseError } from "pocketbase"; import { get, writable, type Writable } from "svelte/store";
import { writable, type Writable } from "svelte/store";
import { fetchGPX } from "./trail_store"; import { fetchGPX } from "./trail_store";
export const lists: Writable<List[]> = writable([]) let lists: List[] = []
export const list: Writable<List | null> = writable(null) export const list: Writable<List | null> = writable(null)
export const listTrail: Writable<Trail | null> = writable(null); export const listTrail: Writable<Trail | null> = writable(null);
export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) { export async function lists_index(filter?: ListFilter, page: number = 1, perPage: number = 5, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const filterText = filter ? buildFilterText(filter) : ""
const r = await f('/api/v1/list?' + new URLSearchParams({ const r = await f('/api/v1/list?' + new URLSearchParams({
sort: `${filter?.sortOrder ?? "-"}${filter?.sort ?? "name"}`, sort: `${filter?.sortOrder ?? "-"}${filter?.sort ?? "name"}`,
"per-page": perPage.toString(),
page: page.toString(),
filter: filterText
}), { }), {
method: 'GET', method: 'GET',
}) })
@@ -21,15 +25,13 @@ export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | UR
throw new ClientResponseError(await r.json()) throw new ClientResponseError(await r.json())
} }
const fetchedLists: List[] = await r.json(); const fetchedLists: ListResult<List> = await r.json();
lists.set(fetchedLists); const result = page > 1 ? [...lists, ...fetchedLists.items] : fetchedLists.items
if (fetchedLists.length > 0) { lists = result;
// list.set(fetchedLists[0])
}
return fetchedLists; return { ...fetchedLists, items: result };
} }
@@ -40,7 +42,7 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
const response = await r.json() const response = await r.json()
for (const trail of response.expand?.trails ?? []) { for (const trail of response.expand?.trails ?? []) {
const gpxData: string = await fetchGPX(trail); const gpxData: string = await fetchGPX(trail, f);
trail.expand.gpx_data = gpxData; trail.expand.gpx_data = gpxData;
} }
@@ -49,7 +51,6 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
throw new ClientResponseError(response) throw new ClientResponseError(response)
} }
list.set(response); list.set(response);
return response; return response;
@@ -85,7 +86,9 @@ export async function lists_create(list: List, avatar?: File) {
body: formData, body: formData,
}) })
if (!r.ok) { if (r.ok) {
return await r.json()
} else {
throw new ClientResponseError(await r.json()) throw new ClientResponseError(await r.json())
} }
} }
@@ -152,4 +155,28 @@ export async function lists_delete(list: List) {
if (!r.ok) { if (!r.ok) {
throw new ClientResponseError(await r.json()) throw new ClientResponseError(await r.json())
} }
}
function buildFilterText(filter: ListFilter): string {
let filterText = `(name~"${filter.q}"||description~"${filter.q}")`
if (filter.author?.length) {
filterText += `&&author="${filter.author}"`
}
if (pb.authStore.model) {
if (filter.public === false && filter.shared === false) {
filterText += `&&author="${pb.authStore.model.id}"`
} else if (filter.public === true && filter.shared === false) {
filterText += `&&(public=true||list_share_via_list.user!="${pb.authStore.model.id}"||author="${pb.authStore.model.id}")`
} else if (filter.public === false && filter.shared === true) {
filterText += `&&(public=false||list_share_via_list.user="${pb.authStore.model.id}"||author="${pb.authStore.model.id}")`
}
}
return filterText
} }

View File

@@ -3,12 +3,35 @@ import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit'; import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) { export async function GET(event: RequestEvent) {
const page = event.url.searchParams.get("page") ?? "0";
const perPage = event.url.searchParams.get("per-page") ?? "10";
const sort = event.url.searchParams.get('sort') ?? "" const sort = event.url.searchParams.get('sort') ?? ""
const filter = event.url.searchParams.get("filter") ?? "";
const expand = "trails,trails.waypoints,trails.category,list_share_via_list";
try { try {
const r: List[] = await pb.collection('lists').getFullList<List>({ let r;
expand: "trails,trails.waypoints,trails.category,list_share_via_list", if (parseInt(perPage) < 0) {
sort: sort, r = {
}) items: await pb.collection('lists')
.getFullList<List>({ expand: expand, sort: sort, filter: filter })
}
} else {
r = await pb.collection('lists')
.getList<List>(parseInt(page), parseInt(perPage), { expand: expand, sort: sort ?? "", filter: filter ?? "" })
}
for (const t of r.items) {
if (!t.author || !pb.authStore.model) {
continue;
}
if (!t.expand) {
t.expand = {}
}
t.expand!.author = await pb.collection("users_anonymous").getOne(t.author);
}
return json(r) return json(r)
} catch (e: any) { } catch (e: any) {
throw error(e.status, e); throw error(e.status, e);

View File

@@ -2,6 +2,12 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/stores"; import { page } from "$app/stores";
import type { DropdownItem } from "$lib/components/base/dropdown.svelte"; import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
import type { SelectItem } from "$lib/components/base/select.svelte";
import Select from "$lib/components/base/select.svelte";
import SkeletonCard from "$lib/components/base/skeleton_card.svelte";
import ConfirmModal from "$lib/components/confirm_modal.svelte"; import ConfirmModal from "$lib/components/confirm_modal.svelte";
import ListCard from "$lib/components/list/list_card.svelte"; import ListCard from "$lib/components/list/list_card.svelte";
import ListPanel from "$lib/components/list/list_panel.svelte"; import ListPanel from "$lib/components/list/list_panel.svelte";
@@ -9,18 +15,29 @@
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte"; import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte"; import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte"; import TrailList from "$lib/components/trail/trail_list.svelte";
import UserSearch from "$lib/components/user_search.svelte";
import { List } from "$lib/models/list"; import { List } from "$lib/models/list";
import type { Trail } from "$lib/models/trail"; import type { Trail } from "$lib/models/trail";
import { import { lists_delete, lists_index } from "$lib/stores/list_store";
lists,
lists_delete,
lists_index
} from "$lib/stores/list_store";
import { fetchGPX } from "$lib/stores/trail_store"; import { fetchGPX } from "$lib/stores/trail_store";
import { currentUser } from "$lib/stores/user_store";
import * as M from "maplibre-gl"; import * as M from "maplibre-gl";
import { onMount, tick } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { slide } from "svelte/transition";
export let data;
const sortOptions: SelectItem[] = [
{ text: $_("alphabetical"), value: "name" },
{ text: $_("creation-date"), value: "created" },
];
let pagination = {
page: data.lists.page,
totalPages: data.lists.totalPages,
};
let openConfirmModal: () => void; let openConfirmModal: () => void;
let openShareModal: () => void; let openShareModal: () => void;
@@ -30,19 +47,28 @@
let markers: M.Marker[]; let markers: M.Marker[];
let showMap: boolean = true; let showMap: boolean = true;
let selectedList: List | null = null; let selectedList: List | null = $page.url.searchParams.get("list")
? data.lists.items[0]
: null;
let selectedTrail: Trail | null = null; let selectedTrail: Trail | null = null;
let activeTrailIndex: number = -1; let activeTrailIndex: number = -1;
let loading: boolean = false;
let loadingNextPage: boolean = false;
let filterExpanded: boolean = false;
let loadAllListsOnNextBack = false;
let userQuery = "";
onMount(() => { onMount(() => {
if ($page.url.searchParams.get("list")) { if ($page.url.searchParams.get("list") && selectedList) {
const listToFocus = $lists.find( setCurrentList(selectedList);
(l) => l.id == $page.url.searchParams.get("list"),
); // only the requested list has been loaded at this point
if (listToFocus) { // load all lists the next time the user presses the back button
setCurrentList(listToFocus); loadAllListsOnNextBack = true;
}
} }
}); });
@@ -66,7 +92,7 @@
return; return;
} }
await lists_delete(selectedList); await lists_delete(selectedList);
await lists_index(); await updateFilter();
selectedList = null; selectedList = null;
} }
@@ -82,9 +108,14 @@
} }
} }
selectedList = item; selectedList = item;
document.getElementById("list-container")?.scrollTo({ top: 0 });
} }
async function back() { async function back() {
if (loadAllListsOnNextBack) {
await updateFilter();
loadAllListsOnNextBack = false;
}
if (selectedTrail) { if (selectedTrail) {
mapWithElevation.unFocusTrail(selectedTrail); mapWithElevation.unFocusTrail(selectedTrail);
selectedTrail = null; selectedTrail = null;
@@ -111,6 +142,80 @@
function unHighlightTrail(trail: Trail) { function unHighlightTrail(trail: Trail) {
mapWithElevation.unHighlightTrail(trail.id!); mapWithElevation.unHighlightTrail(trail.id!);
} }
async function onListScroll(e: any) {
const container = e.target as HTMLDivElement;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
if (
scrollTop + clientHeight >= scrollHeight * 0.8 &&
pagination.page !== pagination.totalPages &&
!loadingNextPage
) {
loadingNextPage = true;
await loadNextPage();
loadingNextPage = false;
}
}
async function loadNextPage() {
pagination.page += 1;
data.lists = await lists_index(data.filter, pagination.page);
}
async function updateFilter() {
loading = true;
if (selectedList || selectedTrail) {
selectedList = null;
selectedTrail = null;
map.flyTo({
animate: true,
zoom: 1,
center: [0, 0],
});
}
pagination.page = 0;
data.lists = await lists_index(data.filter, pagination.page);
loading = false;
}
async function setSort(value: "name" | "created") {
data.filter.sort = value;
await updateFilter();
}
async function setSortOrder() {
if (data.filter.sortOrder === "+") {
data.filter.sortOrder = "-";
} else {
data.filter.sortOrder = "+";
}
await updateFilter();
}
async function setPublicFilter(e: Event) {
data.filter.public = (e.target as HTMLInputElement).checked;
updateFilter();
}
async function setAuthorFilter(item: SearchItem) {
data.filter.author = item.value.id;
await updateFilter();
}
async function clearAuthorFilter() {
data.filter.author = "";
await updateFilter();
}
async function setSharedFilter(e: Event) {
data.filter.shared = (e.target as HTMLInputElement).checked;
updateFilter();
}
</script> </script>
<svelte:head> <svelte:head>
@@ -118,10 +223,10 @@
</svelte:head> </svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 md:mx-4"> <main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 md:mx-4">
<div <div
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full w-full order-1 md:order-none" class="list-list relative md:mx-auto rounded-xl border border-input-border max-h-full w-full order-1 md:order-none"
> >
<div <div
class="flex gap-x-4 items-center justify-between p-4 bg-background z-50 rounded-xl" class="flex gap-x-3 items-center px-3 py-4 bg-background z-50 rounded-xl"
> >
<button <button
class="btn-icon" class="btn-icon"
@@ -129,29 +234,101 @@
disabled={!selectedList} disabled={!selectedList}
on:click={back}><i class="fa fa-arrow-left"></i></button on:click={back}><i class="fa fa-arrow-left"></i></button
> >
<a class="btn-primary btn-large text-center" href="/lists/edit/new" <Search bind:value={data.filter.q} on:update={updateFilter}
><i class="fa fa-plus mr-2"></i>{$_("new-list")}</a ></Search>
<button
class="btn-icon"
on:click={() => (filterExpanded = !filterExpanded)}
><i class="fa fa-sliders"></i></button
>
<a
class="btn-primary tooltip"
data-title={$_("new-list")}
href="/lists/edit/new"><i class="fa fa-plus"></i></a
> >
<!-- <button type="button" class="btn-icon" on:click={toggleMap}
><i class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"
></i></button
> -->
</div> </div>
{#if filterExpanded}
<div
class="absolute bg-background z-10 shadow-lg rounded-b-xl w-full px-14"
in:slide
out:slide
>
<p class="text-sm font-medium pb-1">{$_("sort")}</p>
<div class="flex items-center gap-2">
<Select
bind:value={data.filter.sort}
items={sortOptions}
on:change={(e) => setSort(e.detail)}
></Select>
<button
id="sort-order-btn"
class="btn-icon"
class:rotated={data.filter.sortOrder == "-"}
on:click={() => setSortOrder()}
><i class="fa fa-arrow-up"></i></button
>
</div>
{#if $currentUser}
<hr class="my-4 border-separator" />
<UserSearch
on:click={(e) => setAuthorFilter(e.detail)}
on:clear={clearAuthorFilter}
bind:value={userQuery}
clearAfterSelect={false}
label={$_("author")}
></UserSearch>
<div class="flex items-center my-4">
<input
id="public-checkbox"
type="checkbox"
checked={data.filter.public}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={setPublicFilter}
/>
<label for="public-checkbox" class="ms-2 text-sm"
>{$_("public")}</label
>
</div>
<div class="flex items-center my-4">
<input
id="shared-checkbox"
type="checkbox"
checked={data.filter.shared}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={setSharedFilter}
/>
<label for="shared-checkbox" class="ms-2 text-sm"
>{$_("shared")}</label
>
</div>
{/if}
</div>
{/if}
<hr class="border-separator" /> <hr class="border-separator" />
<div> <div
id="list-container"
class="overflow-y-scroll overflow-x-clip max-h-full"
on:scroll={onListScroll}
>
{#if !selectedList} {#if !selectedList}
<div class="px-4 mt-2"> <div class="px-4 mt-2" class:space-y-3={loading}>
{#each $lists as item, i} {#if loading}
<div {#each { length: 3 } as _, index}
class="list-list-item my-1" <SkeletonCard fullWidth></SkeletonCard>
on:click={() => setCurrentList(item)} {/each}
role="presentation" {:else}
> {#each data.lists.items as item, i}
<ListCard list={item}></ListCard> <div
</div> class="list-list-item"
{/each} on:click={() => setCurrentList(item)}
role="presentation"
>
<ListCard list={item}></ListCard>
</div>
{/each}
{/if}
</div> </div>
{:else if selectedList && !selectedTrail} {:else if selectedList && !selectedTrail}
<ListPanel <ListPanel
@@ -167,7 +344,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<div id="trail-map" class="md:sticky md:top-[62px]" class:hidden={!showMap}> <div id="trail-map" class:hidden={!showMap}>
<MapWithElevationMaplibre <MapWithElevationMaplibre
trails={selectedList?.expand?.trails ?? []} trails={selectedList?.expand?.trails ?? []}
bind:map bind:map
@@ -192,7 +369,13 @@
on:confirm={deleteList} on:confirm={deleteList}
></ConfirmModal> ></ConfirmModal>
{#if selectedList} {#if selectedList}
<ListShareModal list={selectedList} bind:openShareModal <ListShareModal
list={selectedList}
bind:openShareModal
on:update={() => {
data.lists.items = data.lists.items;
selectedList = selectedList;
}}
></ListShareModal> ></ListShareModal>
{/if} {/if}
</main> </main>
@@ -202,5 +385,9 @@
#trail-map { #trail-map {
height: calc(100vh - 124px); height: calc(100vh - 124px);
} }
#list-container {
height: calc(100vh - 204px);
}
} }
</style> </style>

View File

@@ -1,6 +1,35 @@
import { lists_index } from "$lib/stores/list_store"; import type { List, ListFilter } from "$lib/models/list";
import type { Load } from "@sveltejs/kit"; import { lists_index, lists_show } from "$lib/stores/list_store";
import { error, type Load } from "@sveltejs/kit";
import { ClientResponseError, type ListResult } from "pocketbase";
export const load: Load = async ({ params, fetch, url }) => { export const load: Load = async ({ params, fetch, url }) => {
await lists_index(undefined, fetch); const filter: ListFilter = {
q: "",
author: "",
shared: true,
public: true,
sort: "created",
sortOrder: "+",
};
let lists: ListResult<List>;
if (url.searchParams.get("list")) {
try {
const list = await lists_show(url.searchParams.get("list") ?? "", fetch)
lists = { items: [list], page: 1, perPage: 1, totalItems: 1, totalPages: 1 }
} catch (e) {
if (e instanceof ClientResponseError && e.status == 404) {
error(404, {
message: 'Not found'
});
}
throw e
}
} else {
lists = await lists_index(filter, 1, undefined, fetch);
}
return { lists, filter }
}; };

View File

@@ -82,7 +82,8 @@
if ($form.id) { if ($form.id) {
await lists_update($form, avatarFile); await lists_update($form, avatarFile);
} else { } else {
await lists_create($form, avatarFile); const createdList = await lists_create($form, avatarFile);
$form.id = createdList.id;
} }
show_toast({ show_toast({
type: "success", type: "success",

View File

@@ -14,5 +14,4 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
} }
} }
await lists_index(undefined, fetch);
}; };

View File

@@ -1,4 +1,3 @@
import { lists_index } from "$lib/stores/list_store";
import { trails_show } from "$lib/stores/trail_store"; import { trails_show } from "$lib/stores/trail_store";
import { error, type Load } from "@sveltejs/kit"; import { error, type Load } from "@sveltejs/kit";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
@@ -13,7 +12,6 @@ export const load: Load = async ({ params, fetch }) => {
}); });
} }
console.log(e); console.log(e);
} }
await lists_index(undefined, fetch);
}; };

View File

@@ -22,10 +22,9 @@
import { Waypoint } from "$lib/models/waypoint"; import { Waypoint } from "$lib/models/waypoint";
import { categories } from "$lib/stores/category_store"; import { categories } from "$lib/stores/category_store";
import { import {
lists,
lists_add_trail, lists_add_trail,
lists_index, lists_index,
lists_remove_trail, lists_remove_trail
} from "$lib/stores/list_store"; } from "$lib/stores/list_store";
import { summitLog } from "$lib/stores/summit_log_store"; import { summitLog } from "$lib/stores/summit_log_store";
import { show_toast } from "$lib/stores/toast_store"; import { show_toast } from "$lib/stores/toast_store";
@@ -34,6 +33,7 @@
trails_create, trails_create,
trails_update, trails_update,
} from "$lib/stores/trail_store"; } from "$lib/stores/trail_store";
import { currentUser } from "$lib/stores/user_store.js";
import { import {
anchors, anchors,
appendToRoute, appendToRoute,
@@ -53,11 +53,7 @@
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import { import {
fromFile, fromFile,
fromFIT, gpx2trail
fromKML,
fromTCX,
gpx2trail,
isFITFile,
} from "$lib/util/gpx_util"; } from "$lib/util/gpx_util";
import { import {
@@ -73,7 +69,7 @@
import { scale } from "svelte/transition"; import { scale } from "svelte/transition";
import { array, number, object, string } from "yup"; import { array, number, object, string } from "yup";
export let data: { trail: Trail }; export let data;
let map: M.Map; let map: M.Map;
@@ -455,7 +451,7 @@
} else { } else {
await lists_add_trail(list, $form); await lists_add_trail(list, $form);
} }
await lists_index(); await lists_index({ q: "", author: $currentUser?.id ?? "" }, 1, -1);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
show_toast({ show_toast({
@@ -844,13 +840,13 @@
on:click={beforeSummitLogModalOpen} on:click={beforeSummitLogModalOpen}
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button ><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
> >
{#if $lists.length} {#if data.lists.items.length}
<hr class="border-separator" /> <hr class="border-separator" />
<h3 class="text-xl font-semibold"> <h3 class="text-xl font-semibold">
{$_("list", { values: { n: 2 } })} {$_("list", { values: { n: 2 } })}
</h3> </h3>
<div class="flex gap-4 flex-wrap"> <div class="flex gap-4 flex-wrap">
{#each $lists as list} {#each data.lists.items as list}
{#if $form.id && list.trails?.includes($form.id)} {#if $form.id && list.trails?.includes($form.id)}
<div <div
class="flex gap-2 items-center border border-input-border rounded-xl p-2" class="flex gap-2 items-center border border-input-border rounded-xl p-2"
@@ -920,6 +916,7 @@
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog} <SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
></SummitLogModal> ></SummitLogModal>
<ListSelectModal <ListSelectModal
lists={data.lists.items}
bind:openModal={openListSelectModal} bind:openModal={openListSelectModal}
on:change={(e) => handleListSelection(e.detail)} on:change={(e) => handleListSelection(e.detail)}
></ListSelectModal> ></ListSelectModal>

View File

@@ -1,15 +1,16 @@
import { Trail } from "$lib/models/trail"; import { Trail } from "$lib/models/trail";
import { pb } from "$lib/pocketbase";
import { categories_index } from "$lib/stores/category_store"; import { categories_index } from "$lib/stores/category_store";
import { lists_index } from "$lib/stores/list_store"; import { lists_index } from "$lib/stores/list_store";
import { trails_show } from "$lib/stores/trail_store"; import { trails_show } from "$lib/stores/trail_store";
import { error, type Load } from "@sveltejs/kit"; import { error, type Load } from "@sveltejs/kit";
export const load: Load = async ({ params, fetch, data }) => { export const load: Load = async ({ params, fetch }) => {
if (!params.id) { if (!params.id) {
return error(400, "Bad Request") return error(400, "Bad Request")
} }
const categories = await categories_index(fetch) const categories = await categories_index(fetch)
const lists = await lists_index(undefined, fetch) const lists = await lists_index({ q: "", author: pb.authStore.model?.id ?? "" }, 1, -1, fetch)
let trail: Trail; let trail: Trail;
if (params.id === "new") { if (params.id === "new") {
@@ -18,5 +19,5 @@ export const load: Load = async ({ params, fetch, data }) => {
trail = await trails_show(params.id, true, fetch); trail = await trails_show(params.id, true, fetch);
} }
return { trail: trail } return { trail: trail, lists: lists }
}; };

View File

@@ -2,9 +2,11 @@
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte"; import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import { trail } from "$lib/stores/trail_store"; import { trail } from "$lib/stores/trail_store";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
export let data;
</script> </script>
<TrailInfoPanel trail={$trail} mode="overview"></TrailInfoPanel> <TrailInfoPanel trail={data.trail} mode="overview"></TrailInfoPanel>
<svelte:head> <svelte:head>
<title>{$trail.name} | {$_("trail", { values: { n: 1 } })} | wanderer</title <title>{$trail.name} | {$_("trail", { values: { n: 1 } })} | wanderer</title

View File

@@ -1,3 +1,4 @@
import { pb } from "$lib/pocketbase";
import { lists_index } from "$lib/stores/list_store"; import { lists_index } from "$lib/stores/list_store";
import { trails_show } from "$lib/stores/trail_store"; import { trails_show } from "$lib/stores/trail_store";
import { error, type Load } from "@sveltejs/kit"; import { error, type Load } from "@sveltejs/kit";
@@ -5,7 +6,9 @@ import { ClientResponseError } from "pocketbase";
export const load: Load = async ({ params, fetch }) => { export const load: Load = async ({ params, fetch }) => {
try { try {
await trails_show(params.id!, true, fetch) const trail = await trails_show(params.id!, true, fetch)
return { trail }
} catch (e) { } catch (e) {
if (e instanceof ClientResponseError && e.status == 404) { if (e instanceof ClientResponseError && e.status == 404) {
error(404, { error(404, {
@@ -13,7 +16,6 @@ export const load: Load = async ({ params, fetch }) => {
}); });
} }
console.log(e); console.log(e);
} }
await lists_index(undefined, fetch);
}; };