adds lists search and filter
This commit is contained in:
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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