adds search api
This commit is contained in:
87
web/src/lib/components/base/pagination.svelte
Normal file
87
web/src/lib/components/base/pagination.svelte
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { range } from "$lib/util/array_util";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let page: number;
|
||||||
|
export let totalPages: number;
|
||||||
|
|
||||||
|
const maxPagesToRender = 6;
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function update(clickedPage: number) {
|
||||||
|
if (page !== clickedPage) {
|
||||||
|
dispatch("pagination", clickedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: numberGenerator = () => {
|
||||||
|
if (totalPages < 4) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (totalPages <= maxPagesToRender) {
|
||||||
|
return range(totalPages - 2, 2);
|
||||||
|
}
|
||||||
|
if (page < maxPagesToRender - 2) {
|
||||||
|
return range(maxPagesToRender - 1, 2);
|
||||||
|
}
|
||||||
|
if (page > totalPages - 3) {
|
||||||
|
return range(totalPages - 2, totalPages - 5);
|
||||||
|
}
|
||||||
|
if (page > 2 && page <= totalPages - 3) {
|
||||||
|
return range(page + Math.max(1, maxPagesToRender - 5), page - 2);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-x-4 justify-center">
|
||||||
|
{#if totalPages > 1}
|
||||||
|
<button
|
||||||
|
class:text-gray-500={page == 1}
|
||||||
|
disabled={page == 1}
|
||||||
|
on:click={() => update(page - 1)}
|
||||||
|
><i class="fa fa-caret-left"></i></button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||||
|
class:border={page == 1}
|
||||||
|
on:click={() => update(1)}>1</button
|
||||||
|
>
|
||||||
|
{#if totalPages > maxPagesToRender && page >= maxPagesToRender - 2 && page > 2}
|
||||||
|
<span>...</span>
|
||||||
|
{:else if totalPages > 2}
|
||||||
|
<button
|
||||||
|
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||||
|
class:border={page == 2}
|
||||||
|
on:click={() => update(2)}>2</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#each numberGenerator() as i}
|
||||||
|
<button
|
||||||
|
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||||
|
class:border={page == i + 1}
|
||||||
|
on:click={() => update(i + 1)}>{i + 1}</button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
{#if totalPages > maxPagesToRender && page < totalPages - 3}
|
||||||
|
<span>...</span>
|
||||||
|
{:else if totalPages > 3}
|
||||||
|
<button
|
||||||
|
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||||
|
class:border={page == totalPages - 1}
|
||||||
|
on:click={() => update(totalPages - 1)}>{totalPages - 1}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||||
|
class:border={page == totalPages}
|
||||||
|
on:click={() => update(totalPages)}>{totalPages}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class:text-gray-500={page == totalPages}
|
||||||
|
disabled={page == totalPages}
|
||||||
|
on:click={() => update(page + 1)}
|
||||||
|
><i class="fa fa-caret-right"></i></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -90,14 +90,14 @@
|
|||||||
show_toast({
|
show_toast({
|
||||||
type: "success",
|
type: "success",
|
||||||
icon: "check",
|
icon: "check",
|
||||||
text: `Removed trail from "${list.name}"`,
|
text: `${$_("removed-trail-from")} "${list.name}"`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await lists_add_trail(list, trail);
|
await lists_add_trail(list, trail);
|
||||||
show_toast({
|
show_toast({
|
||||||
type: "success",
|
type: "success",
|
||||||
icon: "check",
|
icon: "check",
|
||||||
text: `Added trail to "${list.name}"`,
|
text: `${$_("added-trail-to")} "${list.name}"`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await lists_index();
|
await lists_index();
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
import TrailCard from "./trail_card.svelte";
|
import TrailCard from "./trail_card.svelte";
|
||||||
import TrailListItem from "./trail_list_item.svelte";
|
import TrailListItem from "./trail_list_item.svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
import Pagination from "../base/pagination.svelte";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
|
||||||
export let filter: TrailFilter;
|
export let filter: TrailFilter;
|
||||||
export let trails: Trail[];
|
export let trails: Trail[];
|
||||||
|
export let pagination: { page: number; totalPages: number } = {
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
};
|
||||||
|
|
||||||
const displayOptions: SelectItem[] = [
|
const displayOptions: SelectItem[] = [
|
||||||
{ text: $_('card', {values: {n: 2}}), value: "cards" },
|
{ text: $_("card", { values: { n: 2 } }), value: "cards" },
|
||||||
{ text: $_('list', {values: {n: 1}}), value: "list" },
|
{ text: $_("list", { values: { n: 1 } }), value: "list" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let selectedDisplayOption = displayOptions[0].value;
|
let selectedDisplayOption = displayOptions[0].value;
|
||||||
@@ -20,10 +26,10 @@
|
|||||||
let dispatch = createEventDispatcher();
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const sortOptions: SelectItem[] = [
|
const sortOptions: SelectItem[] = [
|
||||||
{ text: $_('alphabetical'), value: "name" },
|
{ text: $_("alphabetical"), value: "name" },
|
||||||
{ text: $_('creation-date'), value: "created" },
|
{ text: $_("creation-date"), value: "created" },
|
||||||
{ text: $_('distance'), value: "distance" },
|
{ text: $_("distance"), value: "distance" },
|
||||||
{ text: $_('elevation-gain'), value: "elevation_gain" },
|
{ text: $_("elevation-gain"), value: "elevation_gain" },
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -37,11 +43,11 @@
|
|||||||
localStorage.setItem("displayOption", selectedDisplayOption);
|
localStorage.setItem("displayOption", selectedDisplayOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSort() {
|
function setSort() {
|
||||||
dispatch("update", filter);
|
dispatch("update", filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSortOrder() {
|
function setSortOrder() {
|
||||||
if (filter.sortOrder === "+") {
|
if (filter.sortOrder === "+") {
|
||||||
filter.sortOrder = "-";
|
filter.sortOrder = "-";
|
||||||
} else {
|
} else {
|
||||||
@@ -52,9 +58,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-start gap-8 justify-end mx-4">
|
<div class="flex items-end flex-wrap md:flex-nowrap gap-x-6 gap-y-2 mx-4">
|
||||||
<div>
|
<div class="basis-full order-1 md:order-none mt-6 md:mt-0">
|
||||||
<p class="text-sm text-gray-500 pb-2">{$_('sort')}</p>
|
<Pagination
|
||||||
|
page={pagination.page}
|
||||||
|
totalPages={pagination.totalPages}
|
||||||
|
on:pagination
|
||||||
|
></Pagination>
|
||||||
|
</div>
|
||||||
|
<div class="lg:shrink-0">
|
||||||
|
<p class="text-sm text-gray-500 pb-2">{$_("sort")}</p>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
bind:value={filter.sort}
|
bind:value={filter.sort}
|
||||||
@@ -70,8 +83,8 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="lg:shrink-0">
|
||||||
<p class="text-sm text-gray-500 pb-2">{$_('display-as')}</p>
|
<p class="text-sm text-gray-500 pb-2">{$_("display-as")}</p>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
bind:value={selectedDisplayOption}
|
bind:value={selectedDisplayOption}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"add-entry": "Eintrag hinzufügen",
|
"add-entry": "Eintrag hinzufügen",
|
||||||
"add-to-list": "Zu Liste hinzufügen",
|
"add-to-list": "Zu Liste hinzufügen",
|
||||||
"add-waypoint": "Wegpunkt hinzufügen",
|
"add-waypoint": "Wegpunkt hinzufügen",
|
||||||
|
"added-trail-to": "Route hinzugefügt zu",
|
||||||
"alphabetical": "Alphabetisch",
|
"alphabetical": "Alphabetisch",
|
||||||
"already-account": "Du hast bereits ein Konto?",
|
"already-account": "Du hast bereits ein Konto?",
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
@@ -40,11 +41,12 @@
|
|||||||
"english": "Englisch",
|
"english": "Englisch",
|
||||||
"est-duration": "Gesch. Dauer",
|
"est-duration": "Gesch. Dauer",
|
||||||
"explore": "Erkunden",
|
"explore": "Erkunden",
|
||||||
|
"explore-some-trails": "Erkunden Sie einige Routen",
|
||||||
"features": "Feature",
|
"features": "Feature",
|
||||||
"german": "Deutsch",
|
"german": "Deutsch",
|
||||||
"hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!",
|
"hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!",
|
||||||
"hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.",
|
"hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.",
|
||||||
"hero_section_2_text": "Wusstest due schon? Du kannst nicht nur deine Routen speichern. Es gibt viele Kategorien für alle deine Abenteuer.",
|
"hero_section_2_text": "Wusstest du schon? Du kannst nicht nur deine Routen speichern. Es gibt viele Kategorien für alle deine Abenteuer.",
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
"imperial": "Amerikanisch",
|
"imperial": "Amerikanisch",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
@@ -74,6 +76,7 @@
|
|||||||
"public": "Öffentlich",
|
"public": "Öffentlich",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
"register": "Registrieren",
|
"register": "Registrieren",
|
||||||
|
"removed-trail-from": "Route entfernt aus",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"save-trail": "Route speichern",
|
"save-trail": "Route speichern",
|
||||||
"search-cities": "Städte suchen",
|
"search-cities": "Städte suchen",
|
||||||
@@ -88,6 +91,7 @@
|
|||||||
"summit-book": "Gipfelbuch",
|
"summit-book": "Gipfelbuch",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
||||||
|
"trails-for-you": "Routen für dich",
|
||||||
"units": "Einheiten",
|
"units": "Einheiten",
|
||||||
"upload-gpx": "GPX hochladen",
|
"upload-gpx": "GPX hochladen",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"add-entry": "Add Entry",
|
"add-entry": "Add Entry",
|
||||||
"add-to-list": "Add to list",
|
"add-to-list": "Add to list",
|
||||||
"add-waypoint": "Add Waypoint",
|
"add-waypoint": "Add Waypoint",
|
||||||
|
"added-trail-to": "Added trail to",
|
||||||
"alphabetical": "Alphabetical",
|
"alphabetical": "Alphabetical",
|
||||||
"already-account": "Already have an account?",
|
"already-account": "Already have an account?",
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"english": "English",
|
"english": "English",
|
||||||
"est-duration": "Est. duration",
|
"est-duration": "Est. duration",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
|
"explore-some-trails": "Explore some trails",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"german": "German",
|
"german": "German",
|
||||||
"hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!",
|
"hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!",
|
||||||
@@ -74,6 +76,7 @@
|
|||||||
"public": "Public",
|
"public": "Public",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
|
"removed-trail-from": "Removed trail from",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save-trail": "Save Trail",
|
"save-trail": "Save Trail",
|
||||||
"search-cities": "Search cities",
|
"search-cities": "Search cities",
|
||||||
@@ -88,6 +91,7 @@
|
|||||||
"summit-book": "Summit Book",
|
"summit-book": "Summit Book",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
||||||
|
"trails-for-you": "Trails for you",
|
||||||
"units": "Units",
|
"units": "Units",
|
||||||
"upload-gpx": "Upload GPX",
|
"upload-gpx": "Upload GPX",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
import type { Category } from "$lib/models/category";
|
import type { Category } from "$lib/models/category";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
export const categories: Writable<Category[]> = writable([])
|
export const categories: Writable<Category[]> = writable([])
|
||||||
|
|
||||||
export async function categories_index() {
|
export async function categories_index(f: Function = fetch) {
|
||||||
const response: Category[] = (await pb.collection('categories').getFullList<Category>())
|
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);
|
categories.set(response);
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ import type { Waypoint } from "$lib/models/waypoint";
|
|||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { util } from "$lib/vendor/svelte-form-lib/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 { get, writable, type Writable } from "svelte/store";
|
||||||
import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_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 { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store";
|
||||||
import type { LatLng } from "leaflet";
|
|
||||||
import { ClientResponseError } from "pocketbase";
|
|
||||||
|
|
||||||
export const trails: Writable<Trail[]> = writable([])
|
export const trails: Writable<Trail[]> = writable([])
|
||||||
export const trail: Writable<Trail> = writable(new Trail(""));
|
export const trail: Writable<Trail> = writable(new Trail(""));
|
||||||
|
|
||||||
export const editTrail: Writable<Trail> = writable(new Trail(""));
|
export const editTrail: Writable<Trail> = writable(new Trail(""));
|
||||||
|
|
||||||
export async function trails_index(data: { perPage: number, random?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> } = { perPage: 5, random: false, f: fetch }) {
|
export async function trails_index(data: { perPage: number, random?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> } = { perPage: 30, random: false, f: fetch }) {
|
||||||
const r = await data.f('/api/v1/trail?' + new URLSearchParams({
|
const r = await data.f('/api/v1/trail?' + new URLSearchParams({
|
||||||
|
"per-page": data.perPage.toString(),
|
||||||
expand: "category,waypoints,summit_logs",
|
expand: "category,waypoints,summit_logs",
|
||||||
sort: data.random ? "@random" : ""
|
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<Response> = fetch) {
|
export async function trails_search_filter(filter: TrailFilter, page: number = 1, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
let filterText: string = `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.elevationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`;
|
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) {
|
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", {
|
let r = await f("/api/v1/search/trails", {
|
||||||
method: "POST",
|
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();
|
const result = await r.json();
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ export async function trails_search_filter(filter: TrailFilter, f: (url: Request
|
|||||||
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
trails.set(response.items);
|
trails.set(response.items);
|
||||||
return response.items;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
throw new ClientResponseError(response)
|
throw new ClientResponseError(response)
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,6 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
|
|||||||
const response = await r.json()
|
const response = await r.json()
|
||||||
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
|
||||||
for (const trail of response.items) {
|
for (const trail of response.items) {
|
||||||
const gpxData: string = await fetchGPX(trail);
|
const gpxData: string = await fetchGPX(trail);
|
||||||
trail.expand.gpx_data = gpxData;
|
trail.expand.gpx_data = gpxData;
|
||||||
@@ -150,7 +150,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loadGPX) {
|
if (loadGPX) {
|
||||||
const gpxData: string = await fetchGPX(response);
|
const gpxData: string = await fetchGPX(response, f);
|
||||||
response.expand.gpx_data = gpxData;
|
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<Response> = fetch) {
|
||||||
if (!trail.gpx) {
|
if (!trail.gpx) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const gpxUrl = getFileURL(trail, trail.gpx);
|
const gpxUrl = getFileURL(trail, trail.gpx);
|
||||||
const response: Response = await fetch(gpxUrl);
|
const response: Response = await f(gpxUrl);
|
||||||
const gpxData = await response.text();
|
const gpxData = await response.text();
|
||||||
|
|
||||||
return gpxData
|
return gpxData
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ export async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function users_update(id: string, user: User | { [K in keyof User]?: User[K] } | FormData) {
|
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',
|
method: 'POST',
|
||||||
body: JSON.stringify({ id: id, user: user })
|
body: JSON.stringify(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (r.ok) {
|
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) {
|
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',
|
method: 'DELETE',
|
||||||
body: JSON.stringify({ id: user.id })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
3
web/src/lib/util/array_util.ts
Normal file
3
web/src/lib/util/array_util.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function range(to: number, from: number = 0): ReadonlyArray<number> {
|
||||||
|
return [...Array(to - from).keys()].map(i => i + from);
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
import type { FileOptions } from "pocketbase";
|
import type { FileOptions } from "pocketbase";
|
||||||
|
|
||||||
export function getFileURL(record: { [key: string]: any; }, filename?: string, options?: FileOptions) {
|
export function getFileURL(record: { [key: string]: any; }, filename?: string) {
|
||||||
if(!filename) {
|
if (!filename) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return pb.files.getUrl(record, filename, options);
|
|
||||||
|
// return pb.files.getUrl(record, filename);
|
||||||
|
return `/api/v1/files/${record.collectionId}/${record.id}/${filename}`
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
import Toast from "$lib/components/base/toast.svelte";
|
import Toast from "$lib/components/base/toast.svelte";
|
||||||
import Footer from "$lib/components/footer.svelte";
|
import Footer from "$lib/components/footer.svelte";
|
||||||
import NavBar from "$lib/components/nav_bar.svelte";
|
import NavBar from "$lib/components/nav_bar.svelte";
|
||||||
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
import { isRouteProtected } from "$lib/util/authorization_util";
|
import { isRouteProtected } from "$lib/util/authorization_util";
|
||||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||||
import "../css/app.css";
|
import "../css/app.css";
|
||||||
import "../css/components.css";
|
import "../css/components.css";
|
||||||
import "../css/theme.css";
|
import "../css/theme.css";
|
||||||
import { currentUser } from "$lib/stores/user_store";
|
|
||||||
|
|
||||||
beforeNavigate((n) => {
|
beforeNavigate((n) => {
|
||||||
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
|
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
|
||||||
|
|||||||
@@ -36,20 +36,26 @@
|
|||||||
|
|
||||||
const response = await r.json();
|
const response = await r.json();
|
||||||
|
|
||||||
const trailItems = response.results[0].hits.map((t: Record<string, any>) => ({
|
const trailItems = response.results[0].hits.map(
|
||||||
text: t.name,
|
(t: Record<string, any>) => ({
|
||||||
description: `Trail | ${t.location}`,
|
text: t.name,
|
||||||
value: t.id,
|
description: `Trail | ${t.location}`,
|
||||||
icon: "route",
|
value: t.id,
|
||||||
}));
|
icon: "route",
|
||||||
const cityItems = response.results[1].hits.map((c: Record<string, any>) => ({
|
}),
|
||||||
text: c.name,
|
);
|
||||||
description: `City | ${
|
const cityItems = response.results[1].hits.map(
|
||||||
country_codes[c["country code"] as keyof typeof country_codes]
|
(c: Record<string, any>) => ({
|
||||||
}`,
|
text: c.name,
|
||||||
value: c,
|
description: `City | ${
|
||||||
icon: "city",
|
country_codes[
|
||||||
}));
|
c["country code"] as keyof typeof country_codes
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
value: c,
|
||||||
|
icon: "city",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
searchDropdownItems = [...trailItems, ...cityItems];
|
searchDropdownItems = [...trailItems, ...cityItems];
|
||||||
}
|
}
|
||||||
@@ -102,15 +108,15 @@
|
|||||||
id="trails"
|
id="trails"
|
||||||
class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none"
|
class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none"
|
||||||
>
|
>
|
||||||
{#each $trails.splice(0, 5) as trail}
|
{#each { length: Math.min($trails.length, 4) } as _, i}
|
||||||
<a href="/trail/view/{trail.id}">
|
<a href="/trail/view/{$trails[i].id}">
|
||||||
<TrailCard {trail}></TrailCard></a
|
<TrailCard trail={$trails[i]}></TrailCard></a
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-md md:mx-auto space-y-8">
|
<div class="max-w-md md:mx-auto space-y-8">
|
||||||
<h2 class="text-4xl md:text-5xl font-bold">
|
<h2 class="text-4xl md:text-5xl font-bold">
|
||||||
{$currentUser ? "Trails for you" : "Explore some trails"}
|
{$currentUser ? $_("trails-for-you") : $_("explore-some-trails")}
|
||||||
</h2>
|
</h2>
|
||||||
<h5>
|
<h5>
|
||||||
{$_("hero_section_1_text")}
|
{$_("hero_section_1_text")}
|
||||||
@@ -118,6 +124,7 @@
|
|||||||
<a
|
<a
|
||||||
class="inline-block btn-primary btn-large"
|
class="inline-block btn-primary btn-large"
|
||||||
href="/trails"
|
href="/trails"
|
||||||
|
data-sveltekit-preload-data="off"
|
||||||
role="button">{$_("explore")}</a
|
role="button">{$_("explore")}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import type { ServerLoad } from "@sveltejs/kit";
|
|||||||
|
|
||||||
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||||
await trails_index({perPage: 20, random: true, f: fetch})
|
await trails_index({perPage: 20, random: true, f: fetch})
|
||||||
await categories_index()
|
await categories_index(fetch)
|
||||||
};
|
};
|
||||||
13
web/src/routes/api/v1/category/+server.ts
Normal file
13
web/src/routes/api/v1/category/+server.ts
Normal file
@@ -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<Category>()
|
||||||
|
return json(r)
|
||||||
|
} catch (e: any) {
|
||||||
|
throw error(e.status, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
const r = await ms.index(event.params.index as string).search(data.q, data.options);
|
||||||
return json(r);
|
return json(r);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw error(e.status, e)
|
console.log(e);
|
||||||
|
|
||||||
|
throw error(e.httpStatus, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,6 @@ export async function POST(event: RequestEvent) {
|
|||||||
});
|
});
|
||||||
return json(r);
|
return json(r);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw error(e.status, e)
|
throw error(e.httpStatus, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,6 @@ import { pb } from '$lib/pocketbase';
|
|||||||
import type { User } from '$lib/stores/user_store';
|
import type { User } from '$lib/stores/user_store';
|
||||||
import { error, json, type RequestEvent } from '@sveltejs/kit'
|
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<User>(data.id, data.user)
|
|
||||||
return json(r);
|
|
||||||
} catch (e: any) {
|
|
||||||
throw error(e.status, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(event: RequestEvent) {
|
export async function PUT(event: RequestEvent) {
|
||||||
const data = await event.request.json()
|
const data = await event.request.json()
|
||||||
try {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
22
web/src/routes/api/v1/user/[id]/+server.ts
Normal file
22
web/src/routes/api/v1/user/[id]/+server.ts
Normal file
@@ -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<User>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<h5 class="text-xl font-semibold">{$_("create-new-list")}</h5>
|
<h5 class="text-xl font-semibold">{$_("create-new-list")}</h5>
|
||||||
</button>
|
</button>
|
||||||
<hr class="border-separator my-2" />
|
<hr class="border-separator my-2" />
|
||||||
<div class="max-h-[512px] md:max-h-none overflow-y-auto">
|
<div>
|
||||||
{#each $lists as item, i}
|
{#each $lists as item, i}
|
||||||
<li on:click={() => list.set(item)} role="presentation">
|
<li on:click={() => list.set(item)} role="presentation">
|
||||||
<ListCard
|
<ListCard
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { categories_index } from "$lib/stores/category_store";
|
|||||||
import { trails } from "$lib/stores/trail_store";
|
import { trails } from "$lib/stores/trail_store";
|
||||||
import type { ServerLoad } from "@sveltejs/kit";
|
import type { ServerLoad } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ params, locals }) => {
|
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||||
await categories_index()
|
await categories_index(fetch)
|
||||||
|
|
||||||
trails.set([])
|
trails.set([])
|
||||||
};
|
};
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$trail.name} | {$_("map")} | wanderer</title>
|
<title>{$trail.name} | {$_("map")} | wanderer</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[452px_1fr] gap-x-1 gap-y-4">
|
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||||
<div
|
<div
|
||||||
id="trail-details"
|
id="trail-details"
|
||||||
class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 rounded-xl"
|
class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 rounded-xl"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const load: Load = async ({ params, fetch }) => {
|
|||||||
if (!params.id) {
|
if (!params.id) {
|
||||||
return error(400, "Bad Request")
|
return error(400, "Bad Request")
|
||||||
}
|
}
|
||||||
const categories = await categories_index()
|
const categories = await categories_index(fetch)
|
||||||
|
|
||||||
let trail: Trail;
|
let trail: Trail;
|
||||||
if (params.id === "new") {
|
if (params.id === "new") {
|
||||||
|
|||||||
@@ -4,13 +4,17 @@
|
|||||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||||
import type { TrailFilter } from "$lib/models/trail";
|
import type { TrailFilter } from "$lib/models/trail";
|
||||||
import { categories } from "$lib/stores/category_store";
|
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 { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let filterExpanded: boolean = true;
|
let filterExpanded: boolean = true;
|
||||||
|
|
||||||
const filter: TrailFilter = $page.data.filter;
|
const filter: TrailFilter = $page.data.filter;
|
||||||
|
const pagination: {page: number, totalPages: number} = $page.data.pagination
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
@@ -19,7 +23,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function handleFilterUpdate() {
|
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);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -39,6 +50,8 @@
|
|||||||
<TrailList
|
<TrailList
|
||||||
{filter}
|
{filter}
|
||||||
trails={$trails}
|
trails={$trails}
|
||||||
on:update={async () => await trails_search_filter(filter)}
|
pagination={pagination}
|
||||||
|
on:update={() => handleFilterUpdate()}
|
||||||
|
on:pagination={(e) => paginate(e.detail)}
|
||||||
></TrailList>
|
></TrailList>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
|
|||||||
if (paramCategory) {
|
if (paramCategory) {
|
||||||
filter.category.push(paramCategory);
|
filter.category.push(paramCategory);
|
||||||
}
|
}
|
||||||
await trails_search_filter(filter, fetch);
|
const response = await trails_search_filter(filter, 1, fetch);
|
||||||
await categories_index()
|
await categories_index(fetch)
|
||||||
|
|
||||||
return {filter};
|
return {
|
||||||
|
filter: filter, pagination: { page: response.page, totalPages: response.totalPages }
|
||||||
|
};
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user