diff --git a/web/src/lib/components/base/pagination.svelte b/web/src/lib/components/base/pagination.svelte new file mode 100644 index 00000000..70528a2b --- /dev/null +++ b/web/src/lib/components/base/pagination.svelte @@ -0,0 +1,87 @@ + + +
+ {#if totalPages > 1} + + + {#if totalPages > maxPagesToRender && page >= maxPagesToRender - 2 && page > 2} + ... + {:else if totalPages > 2} + + {/if} + {#each numberGenerator() as i} + + {/each} + {#if totalPages > maxPagesToRender && page < totalPages - 3} + ... + {:else if totalPages > 3} + + {/if} + + + {/if} +
diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index a71abac0..b28dab58 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -90,14 +90,14 @@ show_toast({ type: "success", icon: "check", - text: `Removed trail from "${list.name}"`, + text: `${$_("removed-trail-from")} "${list.name}"`, }); } else { await lists_add_trail(list, trail); show_toast({ type: "success", icon: "check", - text: `Added trail to "${list.name}"`, + text: `${$_("added-trail-to")} "${list.name}"`, }); } await lists_index(); diff --git a/web/src/lib/components/trail/trail_list.svelte b/web/src/lib/components/trail/trail_list.svelte index 6a5d597f..61ba6e2d 100644 --- a/web/src/lib/components/trail/trail_list.svelte +++ b/web/src/lib/components/trail/trail_list.svelte @@ -6,13 +6,19 @@ import TrailCard from "./trail_card.svelte"; import TrailListItem from "./trail_list_item.svelte"; import { _ } from "svelte-i18n"; + import Pagination from "../base/pagination.svelte"; + import TextField from "../base/text_field.svelte"; export let filter: TrailFilter; export let trails: Trail[]; + export let pagination: { page: number; totalPages: number } = { + page: 1, + totalPages: 1, + }; const displayOptions: SelectItem[] = [ - { text: $_('card', {values: {n: 2}}), value: "cards" }, - { text: $_('list', {values: {n: 1}}), value: "list" }, + { text: $_("card", { values: { n: 2 } }), value: "cards" }, + { text: $_("list", { values: { n: 1 } }), value: "list" }, ]; let selectedDisplayOption = displayOptions[0].value; @@ -20,10 +26,10 @@ let dispatch = createEventDispatcher(); const sortOptions: SelectItem[] = [ - { text: $_('alphabetical'), value: "name" }, - { text: $_('creation-date'), value: "created" }, - { text: $_('distance'), value: "distance" }, - { text: $_('elevation-gain'), value: "elevation_gain" }, + { text: $_("alphabetical"), value: "name" }, + { text: $_("creation-date"), value: "created" }, + { text: $_("distance"), value: "distance" }, + { text: $_("elevation-gain"), value: "elevation_gain" }, ]; onMount(() => { @@ -37,11 +43,11 @@ localStorage.setItem("displayOption", selectedDisplayOption); } - async function setSort() { + function setSort() { dispatch("update", filter); } - async function setSortOrder() { + function setSortOrder() { if (filter.sortOrder === "+") { filter.sortOrder = "-"; } else { @@ -52,9 +58,16 @@
-
-
-

{$_('sort')}

+
+
+ +
+
+

{$_("sort")}

= writable([]) -export async function categories_index() { - const response: Category[] = (await pb.collection('categories').getFullList()) +export async function categories_index(f: Function = fetch) { + const r = await f('/api/v1/category', { + method: 'GET', + }) + + if (!r.ok) { + throw new ClientResponseError(await r.json()) + } + + const response = await r.json(); categories.set(response); diff --git a/web/src/lib/stores/trail_store.ts b/web/src/lib/stores/trail_store.ts index 12d938b3..c896968b 100644 --- a/web/src/lib/stores/trail_store.ts +++ b/web/src/lib/stores/trail_store.ts @@ -4,19 +4,20 @@ import type { Waypoint } from "$lib/models/waypoint"; import { pb } from "$lib/pocketbase"; import { getFileURL } from "$lib/util/file_util"; import { util } from "$lib/vendor/svelte-form-lib/util"; +import type { LatLng } from "leaflet"; +import { ClientResponseError } from "pocketbase"; import { get, writable, type Writable } from "svelte/store"; import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_store"; import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store"; -import type { LatLng } from "leaflet"; -import { ClientResponseError } from "pocketbase"; export const trails: Writable = writable([]) export const trail: Writable = writable(new Trail("")); export const editTrail: Writable = writable(new Trail("")); -export async function trails_index(data: { perPage: number, random?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise } = { perPage: 5, random: false, f: fetch }) { +export async function trails_index(data: { perPage: number, random?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise } = { perPage: 30, random: false, f: fetch }) { const r = await data.f('/api/v1/trail?' + new URLSearchParams({ + "per-page": data.perPage.toString(), expand: "category,waypoints,summit_logs", sort: data.random ? "@random" : "" }), { @@ -32,7 +33,7 @@ export async function trails_index(data: { perPage: number, random?: boolean, f: } } -export async function trails_search_filter(filter: TrailFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { +export async function trails_search_filter(filter: TrailFilter, page: number = 1, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { let filterText: string = `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.elevationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`; if (filter.category.length > 0) { @@ -46,7 +47,7 @@ export async function trails_search_filter(filter: TrailFilter, f: (url: Request } let r = await f("/api/v1/search/trails", { method: "POST", - body: JSON.stringify({ q: filter.q, options: { filter: filterText } }), + body: JSON.stringify({ q: filter.q, options: { filter: filterText, hitsPerPage: 20, page: page } }), }); const result = await r.json(); @@ -68,7 +69,7 @@ export async function trails_search_filter(filter: TrailFilter, f: (url: Request if (r.ok) { trails.set(response.items); - return response.items; + return result; } else { throw new ClientResponseError(response) } @@ -120,7 +121,6 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L const response = await r.json() if (r.ok) { - for (const trail of response.items) { const gpxData: string = await fetchGPX(trail); trail.expand.gpx_data = gpxData; @@ -150,7 +150,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques } if (loadGPX) { - const gpxData: string = await fetchGPX(response); + const gpxData: string = await fetchGPX(response, f); response.expand.gpx_data = gpxData; } @@ -317,12 +317,12 @@ export async function trails_delete(trail: Trail) { } } -async function fetchGPX(trail: Trail) { +async function fetchGPX(trail: Trail, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { if (!trail.gpx) { return ""; } const gpxUrl = getFileURL(trail, trail.gpx); - const response: Response = await fetch(gpxUrl); + const response: Response = await f(gpxUrl); const gpxData = await response.text(); return gpxData diff --git a/web/src/lib/stores/user_store.ts b/web/src/lib/stores/user_store.ts index 1a80a364..71979bd0 100644 --- a/web/src/lib/stores/user_store.ts +++ b/web/src/lib/stores/user_store.ts @@ -51,9 +51,9 @@ export async function logout() { } export async function users_update(id: string, user: User | { [K in keyof User]?: User[K] } | FormData) { - const r = await fetch('/api/v1/user', { + const r = await fetch('/api/v1/user/' + id, { method: 'POST', - body: JSON.stringify({ id: id, user: user }) + body: JSON.stringify(user) }) if (r.ok) { @@ -65,9 +65,8 @@ export async function users_update(id: string, user: User | { [K in keyof User]? } export async function users_delete(user: User) { - const r = await fetch('/api/v1/user', { + const r = await fetch('/api/v1/user/' + user.id, { method: 'DELETE', - body: JSON.stringify({ id: user.id }) }) if (!r.ok) { diff --git a/web/src/lib/util/array_util.ts b/web/src/lib/util/array_util.ts new file mode 100644 index 00000000..0a70dbde --- /dev/null +++ b/web/src/lib/util/array_util.ts @@ -0,0 +1,3 @@ +export function range(to: number, from: number = 0): ReadonlyArray { + return [...Array(to - from).keys()].map(i => i + from); +} \ No newline at end of file diff --git a/web/src/lib/util/file_util.ts b/web/src/lib/util/file_util.ts index 494370bf..b9abe584 100644 --- a/web/src/lib/util/file_util.ts +++ b/web/src/lib/util/file_util.ts @@ -1,9 +1,11 @@ import { pb } from "$lib/pocketbase"; import type { FileOptions } from "pocketbase"; -export function getFileURL(record: { [key: string]: any; }, filename?: string, options?: FileOptions) { - if(!filename) { +export function getFileURL(record: { [key: string]: any; }, filename?: string) { + if (!filename) { return ""; } - return pb.files.getUrl(record, filename, options); + + // return pb.files.getUrl(record, filename); + return `/api/v1/files/${record.collectionId}/${record.id}/${filename}` } \ No newline at end of file diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index c3ebd940..674b202c 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -3,12 +3,12 @@ import Toast from "$lib/components/base/toast.svelte"; import Footer from "$lib/components/footer.svelte"; import NavBar from "$lib/components/nav_bar.svelte"; + import { currentUser } from "$lib/stores/user_store"; import { isRouteProtected } from "$lib/util/authorization_util"; import "@fortawesome/fontawesome-free/css/all.min.css"; import "../css/app.css"; import "../css/components.css"; import "../css/theme.css"; - import { currentUser } from "$lib/stores/user_store"; beforeNavigate((n) => { if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) { diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 7b113461..040df712 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -36,20 +36,26 @@ const response = await r.json(); - const trailItems = response.results[0].hits.map((t: Record) => ({ - text: t.name, - description: `Trail | ${t.location}`, - value: t.id, - icon: "route", - })); - const cityItems = response.results[1].hits.map((c: Record) => ({ - text: c.name, - description: `City | ${ - country_codes[c["country code"] as keyof typeof country_codes] - }`, - value: c, - icon: "city", - })); + const trailItems = response.results[0].hits.map( + (t: Record) => ({ + text: t.name, + description: `Trail | ${t.location}`, + value: t.id, + icon: "route", + }), + ); + const cityItems = response.results[1].hits.map( + (c: Record) => ({ + text: c.name, + description: `City | ${ + country_codes[ + c["country code"] as keyof typeof country_codes + ] + }`, + value: c, + icon: "city", + }), + ); searchDropdownItems = [...trailItems, ...cityItems]; } @@ -102,15 +108,15 @@ id="trails" class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none" > - {#each $trails.splice(0, 5) as trail} - - + {/each}

- {$currentUser ? "Trails for you" : "Explore some trails"} + {$currentUser ? $_("trails-for-you") : $_("explore-some-trails")}

{$_("hero_section_1_text")} @@ -118,6 +124,7 @@ {$_("explore")}
diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index 3a66c952..6661b61c 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -4,5 +4,5 @@ import type { ServerLoad } from "@sveltejs/kit"; export const load: ServerLoad = async ({ params, locals, fetch }) => { await trails_index({perPage: 20, random: true, f: fetch}) - await categories_index() + await categories_index(fetch) }; \ No newline at end of file diff --git a/web/src/routes/api/v1/category/+server.ts b/web/src/routes/api/v1/category/+server.ts new file mode 100644 index 00000000..6eac8369 --- /dev/null +++ b/web/src/routes/api/v1/category/+server.ts @@ -0,0 +1,13 @@ +import type { Category } from "$lib/models/category"; +import { pb } from "$lib/pocketbase"; +import { error, json, type RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent) { + try { + const r: Category[] = await pb.collection('categories').getFullList() + return json(r) + } catch (e: any) { + throw error(e.status, e); + } + +} \ No newline at end of file diff --git a/web/src/routes/api/v1/files/[collection]/[record]/[file]/+server.ts b/web/src/routes/api/v1/files/[collection]/[record]/[file]/+server.ts new file mode 100644 index 00000000..4439ccfd --- /dev/null +++ b/web/src/routes/api/v1/files/[collection]/[record]/[file]/+server.ts @@ -0,0 +1,23 @@ +import { pb } from "$lib/pocketbase"; +import { error, text, type RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent) { + const parts = []; + parts.push("api"); + parts.push("files"); + parts.push(encodeURIComponent(event.params.collection as string)); + parts.push(encodeURIComponent(event.params.record as string)); + parts.push(encodeURIComponent(event.params.file as string)); + + let fileURL = pb.buildUrl(parts.join("/")); + + try { + const r = await event.fetch(fileURL) + return new Response(r.body, {headers: r.headers}); + } catch (e: any) { + console.log(e); + + throw error(500, e); + } + +} \ No newline at end of file diff --git a/web/src/routes/api/v1/search/[index]/+server.ts b/web/src/routes/api/v1/search/[index]/+server.ts index 81e248db..32e0e190 100644 --- a/web/src/routes/api/v1/search/[index]/+server.ts +++ b/web/src/routes/api/v1/search/[index]/+server.ts @@ -8,6 +8,8 @@ export async function POST(event: RequestEvent) { const r = await ms.index(event.params.index as string).search(data.q, data.options); return json(r); } catch (e: any) { - throw error(e.status, e) + console.log(e); + + throw error(e.httpStatus, e) } } \ No newline at end of file diff --git a/web/src/routes/api/v1/search/multi/+server.ts b/web/src/routes/api/v1/search/multi/+server.ts index 0e1121de..4a43945f 100644 --- a/web/src/routes/api/v1/search/multi/+server.ts +++ b/web/src/routes/api/v1/search/multi/+server.ts @@ -10,6 +10,6 @@ export async function POST(event: RequestEvent) { }); return json(r); } catch (e: any) { - throw error(e.status, e) + throw error(e.httpStatus, e) } } \ No newline at end of file diff --git a/web/src/routes/api/v1/user/+server.ts b/web/src/routes/api/v1/user/+server.ts index 1c4aed26..99a7191a 100644 --- a/web/src/routes/api/v1/user/+server.ts +++ b/web/src/routes/api/v1/user/+server.ts @@ -2,16 +2,6 @@ import { pb } from '$lib/pocketbase'; import type { User } from '$lib/stores/user_store'; import { error, json, type RequestEvent } from '@sveltejs/kit' -export async function POST(event: RequestEvent) { - const data = await event.request.json() - try { - const r = await pb.collection('lists').update(data.id, data.user) - return json(r); - } catch (e: any) { - throw error(e.status, e) - } -} - export async function PUT(event: RequestEvent) { const data = await event.request.json() try { @@ -22,12 +12,3 @@ export async function PUT(event: RequestEvent) { } } -export async function DELETE(event: RequestEvent) { - const data = await event.request.json() - try { - const r = await pb.collection('users').delete(data.id) - return json(r); - } catch (e: any) { - throw error(e.status, e) - } -} diff --git a/web/src/routes/api/v1/user/[id]/+server.ts b/web/src/routes/api/v1/user/[id]/+server.ts new file mode 100644 index 00000000..5a8b9393 --- /dev/null +++ b/web/src/routes/api/v1/user/[id]/+server.ts @@ -0,0 +1,22 @@ +import { pb } from '$lib/pocketbase'; +import type { User } from '$lib/stores/user_store'; +import { error, json, type RequestEvent } from '@sveltejs/kit' + +export async function POST(event: RequestEvent) { + const data = await event.request.json() + try { + const r = await pb.collection('users').update(event.params.id as string, data) + return json(r); + } catch (e: any) { + throw error(e.status, e) + } +} + +export async function DELETE(event: RequestEvent) { + try { + const r = await pb.collection('users').delete(event.params.id as string) + return json(r); + } catch (e: any) { + throw error(e.status, e) + } +} diff --git a/web/src/routes/lists/+page.svelte b/web/src/routes/lists/+page.svelte index 976ed41a..c4436d2f 100644 --- a/web/src/routes/lists/+page.svelte +++ b/web/src/routes/lists/+page.svelte @@ -85,7 +85,7 @@
{$_("create-new-list")}

-
+
{#each $lists as item, i}
  • list.set(item)} role="presentation"> { - await categories_index() +export const load: ServerLoad = async ({ params, locals, fetch }) => { + await categories_index(fetch) trails.set([]) }; \ No newline at end of file diff --git a/web/src/routes/map/trail/[id]/+page.svelte b/web/src/routes/map/trail/[id]/+page.svelte index fc0ccb5b..f302f423 100644 --- a/web/src/routes/map/trail/[id]/+page.svelte +++ b/web/src/routes/map/trail/[id]/+page.svelte @@ -75,7 +75,7 @@ {$trail.name} | {$_("map")} | wanderer -
    +
    { if (!params.id) { return error(400, "Bad Request") } - const categories = await categories_index() + const categories = await categories_index(fetch) let trail: Trail; if (params.id === "new") { diff --git a/web/src/routes/trails/+page.svelte b/web/src/routes/trails/+page.svelte index 5913ff3b..4f50b5fc 100644 --- a/web/src/routes/trails/+page.svelte +++ b/web/src/routes/trails/+page.svelte @@ -4,13 +4,17 @@ import TrailList from "$lib/components/trail/trail_list.svelte"; import type { TrailFilter } from "$lib/models/trail"; import { categories } from "$lib/stores/category_store"; - import { trails, trails_search_filter } from "$lib/stores/trail_store"; + import { + trails, + trails_search_filter, + } from "$lib/stores/trail_store"; import { onMount } from "svelte"; import { _ } from "svelte-i18n"; let filterExpanded: boolean = true; const filter: TrailFilter = $page.data.filter; + const pagination: {page: number, totalPages: number} = $page.data.pagination onMount(() => { if (window.innerWidth < 768) { @@ -19,7 +23,14 @@ }); async function handleFilterUpdate() { - await trails_search_filter(filter); + const response= await trails_search_filter(filter, 1); + pagination.page = response.page + pagination.totalPages = response.totalPages; + } + + async function paginate(page: number) { + pagination.page = page; + const response = await trails_search_filter(filter, page); } @@ -39,6 +50,8 @@ await trails_search_filter(filter)} + pagination={pagination} + on:update={() => handleFilterUpdate()} + on:pagination={(e) => paginate(e.detail)} >
    diff --git a/web/src/routes/trails/+page.ts b/web/src/routes/trails/+page.ts index 432b192b..b17d1cb3 100644 --- a/web/src/routes/trails/+page.ts +++ b/web/src/routes/trails/+page.ts @@ -21,8 +21,10 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => { if (paramCategory) { filter.category.push(paramCategory); } - await trails_search_filter(filter, fetch); - await categories_index() + const response = await trails_search_filter(filter, 1, fetch); + await categories_index(fetch) - return {filter}; + return { + filter: filter, pagination: { page: response.page, totalPages: response.totalPages } + }; }; \ No newline at end of file