adds list search
This commit is contained in:
@@ -119,7 +119,7 @@
|
||||
|
||||
{#if dropDownOpen}
|
||||
<ul
|
||||
class="menu absolute bg-menu-background border border-input-border rounded-xl shadow-md overflow-hidden w-full"
|
||||
class="menu absolute bg-menu-background border border-input-border rounded-xl shadow-md overflow-x-hidden overflow-y-scroll max-h-72 w-full"
|
||||
class:none={!dropDownOpen}
|
||||
style="z-index: 1001"
|
||||
>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
{ text: "Home", value: "/" },
|
||||
{ text: $_("trail", { values: { n: 2 } }), value: "/trails" },
|
||||
{ text: $_("map"), value: "/map" },
|
||||
{ text: $_("list", { values: { n: 2 } }), value: "/lists" },
|
||||
];
|
||||
|
||||
const dropdownItems = [
|
||||
@@ -112,11 +113,6 @@
|
||||
{#each navBarItems as item}
|
||||
<a class="font-semibold text-xl" href={item.value}>{item.text}</a>
|
||||
{/each}
|
||||
{#if $currentUser}
|
||||
<a class="font-semibold text-xl" href="/lists"
|
||||
>{$_("list", { values: { n: 2 } })}</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<hr class="my-6 border-input-border" />
|
||||
<div class="flex flex-col basis-full">
|
||||
@@ -176,11 +172,6 @@
|
||||
{#each navBarItems as item}
|
||||
<a class="font-semibold z-10" href={item.value}>{item.text}</a>
|
||||
{/each}
|
||||
{#if user}
|
||||
<a class="font-semibold z-10" href="/lists"
|
||||
>{$_("list", { values: { n: 2 } })}</a
|
||||
>
|
||||
{/if}
|
||||
</menu>
|
||||
{#if user}
|
||||
<div class="hidden lg:flex gap-6 items-center">
|
||||
@@ -200,10 +191,9 @@
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
onchange={(item) => handleDropdownClick(item)}
|
||||
|
||||
>
|
||||
{#snippet children({ toggleMenu: openDropdown })}
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-10 aspect-square"
|
||||
onclick={openDropdown}
|
||||
@@ -216,8 +206,8 @@
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Dropdown>
|
||||
{/snippet}
|
||||
</Dropdown>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
"link-copied": "Link kopiert",
|
||||
"list": "{n, plural, =1 {Liste} other {Listen}}",
|
||||
"list-not-shared": "Mit niemandem geteilt",
|
||||
"list-public-warning": "Alle routen in dieser Liste werden veröffentlicht.",
|
||||
"list-public-warning": "Alle Routen in dieser Liste werden veröffentlicht.",
|
||||
"list-saved-successfully": "Liste gespeichert",
|
||||
"list-share-warning": "Durch das Teilen einer Liste werden automatisch alle darin enthaltenen Routen freigegeben.",
|
||||
"list-share-warning-update": "Hinzugefügte Routen werden mit allen geteilt, die Zugriff auf diese Liste haben.",
|
||||
@@ -229,7 +229,7 @@
|
||||
"save-trail": "Route speichern",
|
||||
"save-your-trail-first": "Route zuerst speichern",
|
||||
"search-cities": "Städte suchen",
|
||||
"search-for-trails-places": "Suche nach Routen, Orten",
|
||||
"search-for-trails-places": "Suche nach Routen, Listen, Orten",
|
||||
"search-places": "Orte suchen",
|
||||
"search-trails": "Route suchen",
|
||||
"select-list": "Liste auswählen",
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
"save-trail": "Save Trail",
|
||||
"save-your-trail-first": "Save your trail first",
|
||||
"search-cities": "Search cities",
|
||||
"search-for-trails-places": "Search for trails, places",
|
||||
"search-for-trails-places": "Search for trails, lists, places",
|
||||
"search-places": "Search places",
|
||||
"search-trails": "Search trails",
|
||||
"select-list": "Select List",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Language, type Settings } from "../settings";
|
||||
const SettingsCreateSchema = z.object({
|
||||
unit: z.enum(["metric", "imperial"]).optional(),
|
||||
language: z.enum(Object.values(Language) as [Language, ...Language[]]).optional(),
|
||||
bio: z.string().optional(),
|
||||
bio: z.string().optional().nullable(),
|
||||
mapFocus: z.enum(["trails", "location"]).optional(),
|
||||
location: z.object({
|
||||
name: z.string(),
|
||||
@@ -14,14 +14,14 @@ const SettingsCreateSchema = z.object({
|
||||
}).optional(),
|
||||
category: z.string().optional(),
|
||||
tilesets: z.array(z.object({ name: z.string(), url: z.string().url() })).optional(),
|
||||
terrain: z.object({ terrain: z.string().url(), hillshading: z.string().url() }).optional(),
|
||||
terrain: z.object({ terrain: z.string().url(), hillshading: z.string().url() }).optional().nullable(),
|
||||
user: z.string().optional(),
|
||||
privacy: z.object({
|
||||
account: z.enum(["public", "private"]),
|
||||
trails: z.enum(["public", "private"]),
|
||||
lists: z.enum(["public", "private"])
|
||||
}).optional(),
|
||||
notifications: z.record(z.enum(Object.values(NotificationType) as [string, ...string[]]), z.object({ web: z.boolean(), email: z.boolean() })).optional()
|
||||
}).optional().nullable(),
|
||||
notifications: z.record(z.enum(Object.values(NotificationType) as [string, ...string[]]), z.object({ web: z.boolean(), email: z.boolean() })).optional().nullable()
|
||||
|
||||
}) satisfies ZodType<Settings>
|
||||
ZodType<Partial<Comment>>
|
||||
|
||||
@@ -14,18 +14,18 @@ export enum Language {
|
||||
}
|
||||
|
||||
class Settings {
|
||||
id?: string;
|
||||
id?: string | null;
|
||||
unit?: "metric" | "imperial";
|
||||
language?: Language;
|
||||
bio?: string;
|
||||
bio?: string | null;
|
||||
mapFocus?: "trails" | "location";
|
||||
location?: { name: string, lat: number, lon: number };
|
||||
category?: string;
|
||||
tilesets?: { name: string, url: string }[]
|
||||
terrain?: { terrain: string, hillshading: string };
|
||||
tilesets?: ({ name: string, url: string }[]) | null
|
||||
terrain?: { terrain: string, hillshading: string } | null;
|
||||
user?: string;
|
||||
privacy?: { account: "public" | "private", trails: "public" | "private", lists: "public" | "private" }
|
||||
notifications?: Record<NotificationType, { web: boolean, email: boolean }>
|
||||
privacy?: { account: "public" | "private", trails: "public" | "private", lists: "public" | "private" } | null
|
||||
notifications?: Record<NotificationType, { web: boolean, email: boolean }> | null
|
||||
|
||||
constructor(
|
||||
unit: "metric" | "imperial",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type ListResult } from "pocketbase";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { fetchGPX } from "./trail_store";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits } from "meilisearch";
|
||||
|
||||
let lists: List[] = []
|
||||
export const list: Writable<List | null> = writable(null)
|
||||
@@ -34,6 +35,59 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
|
||||
|
||||
lists = result;
|
||||
|
||||
return { ...fetchedLists, items: result };
|
||||
}
|
||||
|
||||
|
||||
export async function lists_search_filter(filter: ListFilter, page: number = 1, perPage: number = 5, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<ListResult<List>> {
|
||||
|
||||
const filterText = buildSearchFilterText(filter)
|
||||
|
||||
let r = await f("/api/v1/search/lists", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q: filter.q,
|
||||
options: {
|
||||
filter: filterText, sort: filter.sort && filter.sortOrder ? [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`] : [],
|
||||
hitsPerPage: perPage,
|
||||
page: page
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const searchResult: { page: number, totalPages: number, hits: Hits<Record<string, any>> } = await r.json();
|
||||
|
||||
|
||||
const listIds = searchResult.hits.map((h: Record<string, any>) => h.id);
|
||||
|
||||
if (listIds.length == 0) {
|
||||
return { items: [], page: searchResult.page, perPage, totalItems: 0, totalPages: searchResult.totalPages };
|
||||
}
|
||||
|
||||
r = await f('/api/v1/list?' + new URLSearchParams({
|
||||
expand: "trails,trails.waypoints,trails.category,list_share_via_list",
|
||||
filter: `'${listIds.join(',')}'~id`,
|
||||
sort: filter.sort && filter.sortOrder ? `${filter.sortOrder}${filter.sort}` : ''
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedLists: ListResult<List> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...lists, ...fetchedLists.items] : fetchedLists.items
|
||||
|
||||
lists = result;
|
||||
|
||||
return { ...fetchedLists, items: result };
|
||||
|
||||
}
|
||||
@@ -78,7 +132,7 @@ export async function lists_create(list: List, avatar?: File) {
|
||||
body: JSON.stringify(list),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
@@ -181,17 +235,12 @@ export async function lists_delete(list: List) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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}"`
|
||||
@@ -201,6 +250,40 @@ function buildFilterText(filter: ListFilter): string {
|
||||
filterText += `&&(public=false||list_share_via_list.user="${pb.authStore.model.id}"||author="${pb.authStore.model.id}")`
|
||||
}
|
||||
}
|
||||
return filterText
|
||||
}
|
||||
|
||||
function buildSearchFilterText(filter: ListFilter): string {
|
||||
let filterText: string = "";
|
||||
|
||||
if (filter.author?.length) {
|
||||
filterText += `author = ${filter.author}`
|
||||
}
|
||||
|
||||
if (filter.public !== undefined || filter.shared !== undefined) {
|
||||
if (filterText.length) {
|
||||
filterText += " AND "
|
||||
}
|
||||
filterText += "("
|
||||
if (filter.public !== undefined) {
|
||||
filterText += `(public = ${filter.public}`
|
||||
|
||||
if (!filter.author?.length || filter.author == pb.authStore.model?.id) {
|
||||
filterText += ` OR author = ${pb.authStore.model?.id}`
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
|
||||
if (filter.shared !== undefined) {
|
||||
if (filter.shared === true) {
|
||||
filterText += ` OR shares = ${pb.authStore.model?.id}`
|
||||
} else {
|
||||
filterText += ` AND NOT shares = ${pb.authStore.model?.id}`
|
||||
|
||||
}
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
|
||||
return filterText
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export type TrailSearchResult = {
|
||||
lat: number,
|
||||
lon: number
|
||||
}
|
||||
auhtor: string;
|
||||
author: string;
|
||||
category: string;
|
||||
completed: boolean;
|
||||
created: number;
|
||||
@@ -33,6 +33,16 @@ export type TrailSearchResult = {
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export type ListSearchResult = {
|
||||
id: string;
|
||||
author: string;
|
||||
created: number;
|
||||
description: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
trails: string[]
|
||||
}
|
||||
|
||||
type NominatimResponse = {
|
||||
type: string
|
||||
licence: string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const privateRoutes = [
|
||||
"/settings",
|
||||
"/lists",
|
||||
"/trail/edit/new",
|
||||
"/profile"
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import {
|
||||
searchMulti,
|
||||
type ListSearchResult,
|
||||
type LocationSearchResult,
|
||||
type TrailSearchResult,
|
||||
} from "$lib/stores/search_store.js";
|
||||
@@ -32,6 +33,11 @@
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "lists",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "locations",
|
||||
q: q,
|
||||
@@ -46,19 +52,27 @@
|
||||
value: t.id,
|
||||
icon: "route",
|
||||
}));
|
||||
const cityItems = r[1].hits.map((c: LocationSearchResult) => ({
|
||||
const listItems = r[1].hits.map((t: ListSearchResult) => ({
|
||||
text: t.name,
|
||||
description: `List, ${t.trails.length} ${$_("trail", { values: { n: t.trails.length } })}`,
|
||||
value: t.id,
|
||||
icon: "layer-group",
|
||||
}));
|
||||
const cityItems = r[2].hits.map((c: LocationSearchResult) => ({
|
||||
text: c.name,
|
||||
description: c.description,
|
||||
value: c,
|
||||
icon: getIconForLocation(c),
|
||||
}));
|
||||
|
||||
searchDropdownItems = [...trailItems, ...cityItems];
|
||||
searchDropdownItems = [...trailItems, ...listItems, ...cityItems];
|
||||
}
|
||||
|
||||
function handleSearchClick(item: SearchItem) {
|
||||
if (item.icon == "route") {
|
||||
goto(`/trail/view/${item.value}`);
|
||||
} else if (item.icon == "layer-group") {
|
||||
goto(`/lists?list=${item.value}`);
|
||||
} else {
|
||||
goto(`/map/?lat=${item.value.lat}&lon=${item.value.lon}`);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
export async function POST(event: RequestEvent) {
|
||||
const data = await event.request.json()
|
||||
|
||||
try {
|
||||
const r = await event.fetch(`${env.NOMINATIM_URL}/search?q=${data.q}&format=geocodejson&limit=${data.limit}`)
|
||||
return json(r);
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
|
||||
throw error(e.httpStatus, e)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,11 @@
|
||||
import UserSearch from "$lib/components/user_search.svelte";
|
||||
import { List, type ListFilter } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { lists_delete, lists_index } from "$lib/stores/list_store";
|
||||
import {
|
||||
lists_delete,
|
||||
lists_index,
|
||||
lists_search_filter,
|
||||
} 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";
|
||||
@@ -53,7 +57,7 @@
|
||||
let showMap: boolean = true;
|
||||
|
||||
let selectedList: List | null = $state(
|
||||
page.url.searchParams.get("list") ? lists.items[0] : null,
|
||||
page.url.searchParams.get("list") ? data.lists.items[0] : null,
|
||||
);
|
||||
let selectedTrail: Trail | null = $state(null);
|
||||
|
||||
@@ -191,8 +195,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
pagination.page = 0;
|
||||
lists = await lists_index(filter, pagination.page);
|
||||
pagination.page = 1;
|
||||
lists = await lists_search_filter(filter, pagination.page);
|
||||
loading = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import {
|
||||
searchMulti,
|
||||
type ListSearchResult,
|
||||
type LocationSearchResult,
|
||||
type TrailSearchResult,
|
||||
} from "$lib/stores/search_store";
|
||||
@@ -56,6 +57,11 @@
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "lists",
|
||||
q: q,
|
||||
limit: 3,
|
||||
},
|
||||
{
|
||||
indexUid: "locations",
|
||||
q: q,
|
||||
@@ -70,19 +76,29 @@
|
||||
value: t,
|
||||
icon: "route",
|
||||
}));
|
||||
const cityItems = r[1].hits.map((c: LocationSearchResult) => ({
|
||||
const listItems = r[1].hits.map((t: ListSearchResult) => ({
|
||||
text: t.name,
|
||||
description: `List, ${t.trails.length} ${$_("trail", { values: { n: t.trails.length } })}`,
|
||||
value: t.id,
|
||||
icon: "layer-group",
|
||||
}));
|
||||
const cityItems = r[2].hits.map((c: LocationSearchResult) => ({
|
||||
text: c.name,
|
||||
description: c.description,
|
||||
value: c,
|
||||
icon: getIconForLocation(c),
|
||||
}));
|
||||
|
||||
searchDropdownItems = [...trailItems, ...cityItems];
|
||||
searchDropdownItems = [...trailItems, ...listItems, ...cityItems];
|
||||
}
|
||||
|
||||
function handleSearchClick(item: SearchItem) {
|
||||
map?.setCenter([item.value.lon, item.value.lat]);
|
||||
map?.setZoom(14);
|
||||
if (item.icon === "layer-group") {
|
||||
goto(`/lists?list=${item.value}`);
|
||||
} else {
|
||||
map?.setCenter([item.value.lon, item.value.lat]);
|
||||
map?.setZoom(14);
|
||||
}
|
||||
}
|
||||
|
||||
async function searchTrails(northEast: M.LngLat, southWest: M.LngLat) {
|
||||
|
||||
Reference in New Issue
Block a user