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
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"
>
<!-- Image placeholder -->

View File

@@ -25,7 +25,7 @@
<button class="btn-secondary" on:click={closeModal}
>{$_("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
>
</div></Modal

View File

@@ -51,18 +51,15 @@
<i class="fa fa-table-list text-5xl"></i>
</div>
{/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">
<h5
class="text-xl font-semibold overflow-hidden overflow-ellipsis basis-full"
class="text-xl font-semibold overflow-hidden overflow-ellipsis"
>
{list.name}
</h5>
{#if list.public}
<span
class="tooltip"
data-title={$_("public")}
>
<span class="tooltip" data-title={$_("public")}>
<i class="fa fa-globe"></i>
</span>
{/if}
@@ -70,6 +67,21 @@
<ShareInfo type="list" subject={list}></ShareInfo>
{/if}
</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
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.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 = [
...(list.author == $currentUser?.id
...(allowShare
? [{ text: $_("share"), value: "share", icon: "share" }]
: []),
...(allowEdit
@@ -122,7 +126,23 @@
<div class="p-4 md:p-6">
<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 />
<div
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 type { List } from "$lib/models/list";
import { lists } from "$lib/stores/list_store";
import { trail } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n";
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 closeModal: (() => void) | undefined = undefined;
@@ -34,7 +35,7 @@
<slot {openModal} />
<ul slot="content">
{#each $lists as list}
{#each lists as list}
<li
class="flex gap-4 items-center p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
on:click={() => handleSelect(list)}

View File

@@ -3,7 +3,6 @@
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,
@@ -11,19 +10,15 @@
list_share_update,
shares,
} from "$lib/stores/list_share_store";
import { lists } from "$lib/stores/list_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 UserSearch from "../user_search.svelte";
@@ -116,8 +111,8 @@
trails: list.expand?.trails ?? [],
list_share_via_list: fetchedShares,
};
lists.set($lists);
sharesLoading = false;
sharesLoading = false;
dispatch("update", list)
}
</script>

View File

@@ -52,7 +52,7 @@
></i>
{#if showInfo}
<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}}
>
{#if loading}

View File

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

View File

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

View File

@@ -1,18 +1,22 @@
import { List, type ListFilter } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import { pb } from "$lib/pocketbase";
import { getFileURL } from "$lib/util/file_util";
import { ClientResponseError } from "pocketbase";
import { writable, type Writable } from "svelte/store";
import { ClientResponseError, type ListResult } from "pocketbase";
import { get, writable, type Writable } from "svelte/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 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({
sort: `${filter?.sortOrder ?? "-"}${filter?.sort ?? "name"}`,
"per-page": perPage.toString(),
page: page.toString(),
filter: filterText
}), {
method: 'GET',
})
@@ -21,15 +25,13 @@ export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | UR
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) {
// list.set(fetchedLists[0])
}
lists = result;
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()
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;
}
@@ -49,7 +51,6 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
throw new ClientResponseError(response)
}
list.set(response);
return response;
@@ -85,7 +86,9 @@ export async function lists_create(list: List, avatar?: File) {
body: formData,
})
if (!r.ok) {
if (r.ok) {
return await r.json()
} else {
throw new ClientResponseError(await r.json())
}
}
@@ -152,4 +155,28 @@ export async function lists_delete(list: List) {
if (!r.ok) {
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
}