adds lists search and filter
This commit is contained in:
@@ -3,12 +3,35 @@ import { pb } from '$lib/pocketbase';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
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 filter = event.url.searchParams.get("filter") ?? "";
|
||||
|
||||
const expand = "trails,trails.waypoints,trails.category,list_share_via_list";
|
||||
|
||||
try {
|
||||
const r: List[] = await pb.collection('lists').getFullList<List>({
|
||||
expand: "trails,trails.waypoints,trails.category,list_share_via_list",
|
||||
sort: sort,
|
||||
})
|
||||
let r;
|
||||
if (parseInt(perPage) < 0) {
|
||||
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)
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e);
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
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 ListCard from "$lib/components/list/list_card.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 TrailInfoPanel from "$lib/components/trail/trail_info_panel.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 type { Trail } from "$lib/models/trail";
|
||||
import {
|
||||
lists,
|
||||
lists_delete,
|
||||
lists_index
|
||||
} from "$lib/stores/list_store";
|
||||
import { lists_delete, lists_index } from "$lib/stores/list_store";
|
||||
import { fetchGPX } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import * as M from "maplibre-gl";
|
||||
|
||||
import { onMount, tick } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
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 openShareModal: () => void;
|
||||
@@ -30,19 +47,28 @@
|
||||
let markers: M.Marker[];
|
||||
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 activeTrailIndex: number = -1;
|
||||
|
||||
let loading: boolean = false;
|
||||
let loadingNextPage: boolean = false;
|
||||
let filterExpanded: boolean = false;
|
||||
|
||||
let loadAllListsOnNextBack = false;
|
||||
|
||||
let userQuery = "";
|
||||
|
||||
onMount(() => {
|
||||
if ($page.url.searchParams.get("list")) {
|
||||
const listToFocus = $lists.find(
|
||||
(l) => l.id == $page.url.searchParams.get("list"),
|
||||
);
|
||||
if (listToFocus) {
|
||||
setCurrentList(listToFocus);
|
||||
}
|
||||
if ($page.url.searchParams.get("list") && selectedList) {
|
||||
setCurrentList(selectedList);
|
||||
|
||||
// only the requested list has been loaded at this point
|
||||
// load all lists the next time the user presses the back button
|
||||
loadAllListsOnNextBack = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,7 +92,7 @@
|
||||
return;
|
||||
}
|
||||
await lists_delete(selectedList);
|
||||
await lists_index();
|
||||
await updateFilter();
|
||||
selectedList = null;
|
||||
}
|
||||
|
||||
@@ -82,9 +108,14 @@
|
||||
}
|
||||
}
|
||||
selectedList = item;
|
||||
document.getElementById("list-container")?.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
async function back() {
|
||||
if (loadAllListsOnNextBack) {
|
||||
await updateFilter();
|
||||
loadAllListsOnNextBack = false;
|
||||
}
|
||||
if (selectedTrail) {
|
||||
mapWithElevation.unFocusTrail(selectedTrail);
|
||||
selectedTrail = null;
|
||||
@@ -111,6 +142,80 @@
|
||||
function unHighlightTrail(trail: Trail) {
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
@@ -118,10 +223,10 @@
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 md:mx-4">
|
||||
<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
|
||||
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
|
||||
class="btn-icon"
|
||||
@@ -129,29 +234,101 @@
|
||||
disabled={!selectedList}
|
||||
on:click={back}><i class="fa fa-arrow-left"></i></button
|
||||
>
|
||||
<a class="btn-primary btn-large text-center" href="/lists/edit/new"
|
||||
><i class="fa fa-plus mr-2"></i>{$_("new-list")}</a
|
||||
<Search bind:value={data.filter.q} on:update={updateFilter}
|
||||
></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>
|
||||
{#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" />
|
||||
|
||||
<div>
|
||||
<div
|
||||
id="list-container"
|
||||
class="overflow-y-scroll overflow-x-clip max-h-full"
|
||||
on:scroll={onListScroll}
|
||||
>
|
||||
{#if !selectedList}
|
||||
<div class="px-4 mt-2">
|
||||
{#each $lists as item, i}
|
||||
<div
|
||||
class="list-list-item my-1"
|
||||
on:click={() => setCurrentList(item)}
|
||||
role="presentation"
|
||||
>
|
||||
<ListCard list={item}></ListCard>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="px-4 mt-2" class:space-y-3={loading}>
|
||||
{#if loading}
|
||||
{#each { length: 3 } as _, index}
|
||||
<SkeletonCard fullWidth></SkeletonCard>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each data.lists.items as item, i}
|
||||
<div
|
||||
class="list-list-item"
|
||||
on:click={() => setCurrentList(item)}
|
||||
role="presentation"
|
||||
>
|
||||
<ListCard list={item}></ListCard>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if selectedList && !selectedTrail}
|
||||
<ListPanel
|
||||
@@ -167,7 +344,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div id="trail-map" class="md:sticky md:top-[62px]" class:hidden={!showMap}>
|
||||
<div id="trail-map" class:hidden={!showMap}>
|
||||
<MapWithElevationMaplibre
|
||||
trails={selectedList?.expand?.trails ?? []}
|
||||
bind:map
|
||||
@@ -192,7 +369,13 @@
|
||||
on:confirm={deleteList}
|
||||
></ConfirmModal>
|
||||
{#if selectedList}
|
||||
<ListShareModal list={selectedList} bind:openShareModal
|
||||
<ListShareModal
|
||||
list={selectedList}
|
||||
bind:openShareModal
|
||||
on:update={() => {
|
||||
data.lists.items = data.lists.items;
|
||||
selectedList = selectedList;
|
||||
}}
|
||||
></ListShareModal>
|
||||
{/if}
|
||||
</main>
|
||||
@@ -202,5 +385,9 @@
|
||||
#trail-map {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
|
||||
#list-container {
|
||||
height: calc(100vh - 204px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
import { lists_index } from "$lib/stores/list_store";
|
||||
import type { Load } from "@sveltejs/kit";
|
||||
import type { List, ListFilter } from "$lib/models/list";
|
||||
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 }) => {
|
||||
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 }
|
||||
};
|
||||
@@ -82,7 +82,8 @@
|
||||
if ($form.id) {
|
||||
await lists_update($form, avatarFile);
|
||||
} else {
|
||||
await lists_create($form, avatarFile);
|
||||
const createdList = await lists_create($form, avatarFile);
|
||||
$form.id = createdList.id;
|
||||
}
|
||||
show_toast({
|
||||
type: "success",
|
||||
|
||||
@@ -14,5 +14,4 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||
}
|
||||
|
||||
}
|
||||
await lists_index(undefined, fetch);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { lists_index } from "$lib/stores/list_store";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
import { error, type Load } from "@sveltejs/kit";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
@@ -13,7 +12,6 @@ export const load: Load = async ({ params, fetch }) => {
|
||||
});
|
||||
}
|
||||
console.log(e);
|
||||
|
||||
|
||||
}
|
||||
await lists_index(undefined, fetch);
|
||||
};
|
||||
@@ -22,10 +22,9 @@
|
||||
import { Waypoint } from "$lib/models/waypoint";
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import {
|
||||
lists,
|
||||
lists_add_trail,
|
||||
lists_index,
|
||||
lists_remove_trail,
|
||||
lists_remove_trail
|
||||
} from "$lib/stores/list_store";
|
||||
import { summitLog } from "$lib/stores/summit_log_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
@@ -34,6 +33,7 @@
|
||||
trails_create,
|
||||
trails_update,
|
||||
} from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store.js";
|
||||
import {
|
||||
anchors,
|
||||
appendToRoute,
|
||||
@@ -53,11 +53,7 @@
|
||||
} from "$lib/util/format_util";
|
||||
import {
|
||||
fromFile,
|
||||
fromFIT,
|
||||
fromKML,
|
||||
fromTCX,
|
||||
gpx2trail,
|
||||
isFITFile,
|
||||
gpx2trail
|
||||
} from "$lib/util/gpx_util";
|
||||
|
||||
import {
|
||||
@@ -73,7 +69,7 @@
|
||||
import { scale } from "svelte/transition";
|
||||
import { array, number, object, string } from "yup";
|
||||
|
||||
export let data: { trail: Trail };
|
||||
export let data;
|
||||
|
||||
let map: M.Map;
|
||||
|
||||
@@ -455,7 +451,7 @@
|
||||
} else {
|
||||
await lists_add_trail(list, $form);
|
||||
}
|
||||
await lists_index();
|
||||
await lists_index({ q: "", author: $currentUser?.id ?? "" }, 1, -1);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_toast({
|
||||
@@ -844,13 +840,13 @@
|
||||
on:click={beforeSummitLogModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
|
||||
>
|
||||
{#if $lists.length}
|
||||
{#if data.lists.items.length}
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">
|
||||
{$_("list", { values: { n: 2 } })}
|
||||
</h3>
|
||||
<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)}
|
||||
<div
|
||||
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>
|
||||
<ListSelectModal
|
||||
lists={data.lists.items}
|
||||
bind:openModal={openListSelectModal}
|
||||
on:change={(e) => handleListSelection(e.detail)}
|
||||
></ListSelectModal>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Trail } from "$lib/models/trail";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { categories_index } from "$lib/stores/category_store";
|
||||
import { lists_index } from "$lib/stores/list_store";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
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) {
|
||||
return error(400, "Bad Request")
|
||||
}
|
||||
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;
|
||||
if (params.id === "new") {
|
||||
@@ -18,5 +19,5 @@ export const load: Load = async ({ params, fetch, data }) => {
|
||||
trail = await trails_show(params.id, true, fetch);
|
||||
}
|
||||
|
||||
return { trail: trail }
|
||||
return { trail: trail, lists: lists }
|
||||
};
|
||||
@@ -2,9 +2,11 @@
|
||||
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<TrailInfoPanel trail={$trail} mode="overview"></TrailInfoPanel>
|
||||
<TrailInfoPanel trail={data.trail} mode="overview"></TrailInfoPanel>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$trail.name} | {$_("trail", { values: { n: 1 } })} | wanderer</title
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { lists_index } from "$lib/stores/list_store";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
import { error, type Load } from "@sveltejs/kit";
|
||||
@@ -5,7 +6,9 @@ import { ClientResponseError } from "pocketbase";
|
||||
|
||||
export const load: Load = async ({ params, fetch }) => {
|
||||
try {
|
||||
await trails_show(params.id!, true, fetch)
|
||||
const trail = await trails_show(params.id!, true, fetch)
|
||||
|
||||
return { trail }
|
||||
} catch (e) {
|
||||
if (e instanceof ClientResponseError && e.status == 404) {
|
||||
error(404, {
|
||||
@@ -13,7 +16,6 @@ export const load: Load = async ({ params, fetch }) => {
|
||||
});
|
||||
}
|
||||
console.log(e);
|
||||
|
||||
}
|
||||
await lists_index(undefined, fetch);
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user