adds trail author filter

This commit is contained in:
Christian Beutel
2024-11-15 23:57:09 +01:00
parent 24ef5998c0
commit f260b63d43
27 changed files with 345 additions and 153 deletions

View File

@@ -39,7 +39,7 @@
</label> </label>
{/if} {/if}
<button <button
class="min-w-44 flex flex-wrap items-center gap-2 border border-input-border bg-input-background min-h-[50px] p-3 rounded-md transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0" class="min-w-44 w-full flex flex-wrap items-center gap-2 border border-input-border bg-input-background min-h-[50px] p-3 rounded-md transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0"
on:click={() => (showDropdown = !showDropdown)} on:click={() => (showDropdown = !showDropdown)}
> >
{#if value.length === 0} {#if value.length === 0}

View File

@@ -18,6 +18,8 @@
export let placeholder: string = "Search..."; export let placeholder: string = "Search...";
export let large: boolean = false; export let large: boolean = false;
export let extraClasses: string = ""; export let extraClasses: string = "";
export let label: string = "";
export let clearAfterSelect: boolean = true;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -46,23 +48,32 @@
function handleItemClick(item: SearchItem) { function handleItemClick(item: SearchItem) {
searching = false; searching = false;
dispatch("click", item); dispatch("click", item);
clear(); if (clearAfterSelect) {
clear();
}
} }
function clear() { function clear() {
value = ""; value = "";
items = [];
update(value); update(value);
} }
</script> </script>
<div class="relative {extraClasses}"> <div class="relative {extraClasses}">
<span class="absolute top-1/2 -translate-y-1/2 left-0 pl-4"> <span
class="absolute {large
? 'bottom-[31px]'
: 'bottom-[25px]'} translate-y-1/2 left-0 pl-4"
>
<i class="fa fa-search" class:text-xl={large}></i> <i class="fa fa-search" class:text-xl={large}></i>
</span> </span>
{#if value.length > 0} {#if value.length > 0}
<button <button
type="button" type="button"
class="btn-icon absolute top-1/2 -translate-y-1/2 right-0 mr-2" class="btn-icon absolute {large
? 'bottom-[31px]'
: 'bottom-[25px]'} translate-y-1/2 right-0 mr-2"
on:click={clear} on:click={clear}
in:fade={{ duration: 150 }} in:fade={{ duration: 150 }}
out:fade={{ duration: 150 }} out:fade={{ duration: 150 }}
@@ -74,6 +85,7 @@
type="search" type="search"
name="q" name="q"
autocomplete="off" autocomplete="off"
{label}
extraClasses={large extraClasses={large
? "px-12 py-4 text-xl min-w-80 w-[33vw] max-w-[532px] rounded-xl" ? "px-12 py-4 text-xl min-w-80 w-[33vw] max-w-[532px] rounded-xl"
: "px-10"} : "px-10"}

View File

@@ -5,11 +5,11 @@
import { TrailShare } from "$lib/models/trail_share"; import { TrailShare } from "$lib/models/trail_share";
import type { User } from "$lib/models/user"; import type { User } from "$lib/models/user";
import { import {
list_share_create, list_share_create,
list_share_delete, list_share_delete,
list_share_index, list_share_index,
list_share_update, list_share_update,
shares shares,
} from "$lib/stores/list_share_store"; } from "$lib/stores/list_share_store";
import { lists } from "$lib/stores/list_store"; import { lists } from "$lib/stores/list_store";
import { show_toast } from "$lib/stores/toast_store"; import { show_toast } from "$lib/stores/toast_store";
@@ -26,6 +26,7 @@
import Search, { type SearchItem } from "../base/search.svelte"; import Search, { type SearchItem } from "../base/search.svelte";
import type { SelectItem } from "../base/select.svelte"; import type { SelectItem } from "../base/select.svelte";
import Select from "../base/select.svelte"; import Select from "../base/select.svelte";
import UserSearch from "../user_search.svelte";
let openModal: (() => void) | undefined = undefined; let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined; export let closeModal: (() => void) | undefined = undefined;
@@ -43,8 +44,6 @@
let copyButtonText = $_("copy-link"); let copyButtonText = $_("copy-link");
let searchItems: SearchItem[] = [];
let sharesLoading: boolean = false; let sharesLoading: boolean = false;
const permissionSelectItems: SelectItem[] = [ const permissionSelectItems: SelectItem[] = [
@@ -52,33 +51,16 @@
{ text: $_("edit"), value: "edit" }, { text: $_("edit"), value: "edit" },
]; ];
async function updateUsers(q: string) {
try {
const users: User[] = await users_search(q);
searchItems = users.map((u) => ({
text: u.username!,
value: u,
icon: "user",
}));
} catch (e) {
console.error(e);
show_toast({
type: "error",
icon: "close",
text: "Error during search",
});
}
}
function copyURLToClipboard() { function copyURLToClipboard() {
navigator.clipboard.writeText(window.location.href.split('?')[0] + '?list=' + list.id); navigator.clipboard.writeText(
window.location.href.split("?")[0] + "?list=" + list.id,
);
copyButtonText = $_("link-copied"); copyButtonText = $_("link-copied");
setTimeout(() => (copyButtonText = $_("copy-link")), 3000); setTimeout(() => (copyButtonText = $_("copy-link")), 3000);
} }
function close() { function close() {
searchItems = [];
dispatch("save"); dispatch("save");
closeModal!(); closeModal!();
} }
@@ -134,7 +116,7 @@
trails: list.expand?.trails ?? [], trails: list.expand?.trails ?? [],
list_share_via_list: fetchedShares, list_share_via_list: fetchedShares,
}; };
lists.set($lists) lists.set($lists);
sharesLoading = false; sharesLoading = false;
} }
</script> </script>
@@ -150,21 +132,7 @@
<p class="p-4 bg-amber-100 rounded-xl mb-4 text-sm text-gray-500"> <p class="p-4 bg-amber-100 rounded-xl mb-4 text-sm text-gray-500">
{$_("list-share-warning")} {$_("list-share-warning")}
</p> </p>
<Search <UserSearch includeSelf={false} on:click={(e) => shareList(e.detail)}></UserSearch>
on:update={(e) => updateUsers(e.detail)}
on:click={(e) => shareList(e.detail)}
placeholder={`${$_("username")}`}
items={searchItems}
>
<img
slot="item-header"
let:item
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(item.value, item.value.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${item.value.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
</Search>
<h4 class="font-semibold mt-4">{$_("shared-with")}</h4> <h4 class="font-semibold mt-4">{$_("shared-with")}</h4>
{#if $shares.length == 0} {#if $shares.length == 0}

View File

@@ -8,6 +8,7 @@
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import ShareInfo from "../share_info.svelte"; import ShareInfo from "../share_info.svelte";
import { pb } from "$lib/pocketbase";
export let trail: Trail; export let trail: Trail;
@@ -27,15 +28,15 @@
<div <div
class="relative w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl" class="relative w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl"
> >
<img src={thumbnail} alt="" /> <img id="header-img" src={thumbnail} alt="" />
</div> </div>
{#if trail.public || trailIsShared} {#if (trail.public || trailIsShared) && pb.authStore.model}
<div <div
class="flex absolute top-4 right-4 {trail.public && trailIsShared class="flex absolute top-4 right-4 {trail.public && trailIsShared
? 'w-14' ? 'w-14'
: 'w-8'} h-8 rounded-full items-center justify-center bg-background text-content" : 'w-8'} h-8 rounded-full items-center justify-center bg-background text-content"
> >
{#if trail.public} {#if trail.public && pb.authStore.model}
<span <span
class="tooltip" class="tooltip"
class:mr-2={trail.public && trailIsShared} class:mr-2={trail.public && trailIsShared}
@@ -62,6 +63,20 @@
})} })}
</p> </p>
{/if} {/if}
{#if trail.expand.author}
<p class="text-xs text-gray-500 mb-3">
{$_("by")} <img
class="rounded-full w-5 aspect-square mx-1 inline"
src={getFileURL(
trail.expand.author,
trail.expand.author.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
{trail.expand.author.username}
</p>
{/if}
<div class="flex gap-x-4"> <div class="flex gap-x-4">
{#if trail.location} {#if trail.location}
<h5> <h5>
@@ -75,7 +90,9 @@
</h5> </h5>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 mt-2 gap-1 text-sm text-gray-500 whitespace-nowrap"> <div
class="grid grid-cols-2 mt-2 gap-1 text-sm text-gray-500 whitespace-nowrap"
>
<span <span
><i class="fa fa-left-right mr-2"></i>{formatDistance( ><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance, trail.distance,
@@ -101,12 +118,12 @@
</div> </div>
<style> <style>
.trail-card img { .trail-card #header-img {
object-fit: cover; object-fit: cover;
transition: 0.25s ease; transition: 0.25s ease;
} }
.trail-card:hover img { .trail-card:hover #header-img {
scale: 1.075; scale: 1.075;
} }
</style> </style>

View File

@@ -6,13 +6,16 @@
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
import Datepicker from "../base/datepicker.svelte";
import DoubleSlider from "../base/double_slider.svelte"; import DoubleSlider from "../base/double_slider.svelte";
import MultiSelect from "../base/multi_select.svelte";
import type { RadioItem } from "../base/radio_group.svelte"; import type { RadioItem } from "../base/radio_group.svelte";
import RadioGroup from "../base/radio_group.svelte"; import RadioGroup from "../base/radio_group.svelte";
import Search, { type SearchItem } from "../base/search.svelte"; import Search, { type SearchItem } from "../base/search.svelte";
import type { SelectItem } from "../base/select.svelte"; import type { SelectItem } from "../base/select.svelte";
import Slider from "../base/slider.svelte"; import Slider from "../base/slider.svelte";
import Datepicker from "../base/datepicker.svelte"; import UserSearch from "../user_search.svelte";
import { pb } from "$lib/pocketbase";
export let categories: Category[]; export let categories: Category[];
export let filterExpanded: boolean = true; export let filterExpanded: boolean = true;
@@ -20,6 +23,11 @@
export let showTrailSearch: boolean = true; export let showTrailSearch: boolean = true;
export let showCitySearch: boolean = true; export let showCitySearch: boolean = true;
$: categorySelectItems = categories.map((c) => ({
value: c.id,
text: c.name,
}));
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const radioGroupItems: RadioItem[] = [ const radioGroupItems: RadioItem[] = [
@@ -42,31 +50,29 @@
dispatch("update", filter); dispatch("update", filter);
} }
function setCategoryFilter(category: Category) { function setCategoryFilter(categories: SelectItem[]) {
const categoryIndex = filter.category.findIndex( filter.category = categories.map((c) => c.value);
(c) => c == category.id,
);
if (categoryIndex !== -1) {
filter.category.splice(categoryIndex, 1);
} else {
filter.category.push(category.id);
}
update(); update();
} }
function setDifficultyFilter( function setAuthorFilter(item: SearchItem) {
difficulty: "easy" | "moderate" | "difficult", filter.author = item.value.id;
) { update();
const difficultyIndex = filter.difficulty.findIndex( }
(d) => d == difficulty,
);
if (difficultyIndex !== -1) {
filter.difficulty.splice(difficultyIndex, 1);
} else {
filter.difficulty.push(difficulty);
}
function setDifficultyFilter(difficulties: SelectItem[]) {
filter.difficulty = difficulties.map((d) => d.value);
update();
}
function setPublicFilter(e: Event) {
filter.public = (e.target as HTMLInputElement).checked;
update();
}
function setSharedFilter(e: Event) {
filter.shared = (e.target as HTMLInputElement).checked;
update(); update();
} }
@@ -145,47 +151,63 @@
{#if showTrailSearch} {#if showTrailSearch}
<hr class="my-4 border-separator" /> <hr class="my-4 border-separator" />
{/if} {/if}
<p class="text-sm font-medium pb-4">{$_("category")}</p> <MultiSelect
{#each categories as category, i} on:change={(e) => setCategoryFilter(e.detail)}
<div class="flex items-center mb-4"> label={$_("categories")}
<input items={categorySelectItems}
id="{category.name}-checkbox" placeholder={`${$_("filter-categories")}...`}
type="checkbox" ></MultiSelect>
checked={filter.category.includes(category.id)}
value={category.id}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={() => setCategoryFilter(category)}
/>
<label for="{category.name}-checkbox" class="ms-2 text-sm"
>{$_(category.name)}</label
>
</div>
{/each}
<hr class="my-4 border-separator" /> <hr class="my-4 border-separator" />
<p class="text-sm font-medium pb-4">{$_("difficulty")}</p> {#if pb.authStore.model}
{#each difficultyItems as difficulty, i} <UserSearch
<div class="flex items-center mb-4"> on:click={(e) => setAuthorFilter(e.detail)}
on:clear={() => {
filter.author = "";
update();
}}
clearAfterSelect={false}
label={$_("author")}
></UserSearch>
<div class="flex items-center my-4">
<input <input
id="{difficulty.value}-checkbox" id="public-checkbox"
type="checkbox" type="checkbox"
checked={filter.difficulty.includes(difficulty.value)} checked={filter.public}
value={difficulty.value}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2" class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
on:change={() => setDifficultyFilter(difficulty.value)} on:change={setPublicFilter}
/> />
<label <label for="public-checkbox" class="ms-2 text-sm"
for="{difficulty.value}-checkbox" >{$_("public")}</label
class="ms-2 text-sm">{difficulty.text}</label
> >
</div> </div>
{/each} <div class="flex items-center my-4">
<input
id="shared-checkbox"
type="checkbox"
checked={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>
<hr class="my-4 border-separator" />
{/if}
<MultiSelect
on:change={(e) => setDifficultyFilter(e.detail)}
label={$_("difficulty")}
items={difficultyItems}
placeholder={`${$_("filter-difficulty")}...`}
></MultiSelect>
<hr class="my-4 border-separator" /> <hr class="my-4 border-separator" />
{#if showCitySearch} {#if showCitySearch}
<p class="text-sm font-medium pb-4">{$_("near")}</p>
<div class="mb-8"> <div class="mb-8">
<Search <Search
items={searchDropdownItems} items={searchDropdownItems}
label={$_("near")}
placeholder="{$_('search-cities')}..." placeholder="{$_('search-cities')}..."
clearAfterSelect={false}
bind:value={citySearchQuery} bind:value={citySearchQuery}
on:update={(e) => searchCities(e.detail)} on:update={(e) => searchCities(e.detail)}
on:click={(e) => handleSearchClick(e.detail)} on:click={(e) => handleSearchClick(e.detail)}

View File

@@ -38,6 +38,7 @@
import PhotoGallery from "../photo_gallery.svelte"; import PhotoGallery from "../photo_gallery.svelte";
import ShareInfo from "../share_info.svelte"; import ShareInfo from "../share_info.svelte";
import SummitLogTable from "../summit_log/summit_log_table.svelte"; import SummitLogTable from "../summit_log/summit_log_table.svelte";
import { pb } from "$lib/pocketbase";
export let trail: Trail; export let trail: Trail;
export let mode: "overview" | "map" | "list" = "map"; export let mode: "overview" | "map" | "list" = "map";
@@ -184,14 +185,14 @@
<div <div
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50" class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
></div> ></div>
{#if trail.public || trailIsShared} {#if (trail.public || trailIsShared) && pb.authStore.model}
<div <div
class="flex absolute top-8 right-6 {trail.public && class="flex absolute top-8 right-6 {trail.public &&
trailIsShared trailIsShared
? 'w-16' ? 'w-16'
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary" : 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
> >
{#if trail.public} {#if trail.public && pb.authStore.model}
<span <span
class="tooltip text-2xl" class="tooltip text-2xl"
class:mr-3={trail.public && trailIsShared} class:mr-3={trail.public && trailIsShared}
@@ -226,6 +227,21 @@
)} )}
</h5> </h5>
{/if} {/if}
{#if trail.expand.author}
<p class="my-3">
{$_("by")}
<img
class="rounded-full w-8 aspect-square mx-1 inline"
src={getFileURL(
trail.expand.author,
trail.expand.author.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
{trail.expand.author.username}
</p>
{/if}
<div class="flex flex-wrap gap-x-8 gap-y-2 mt-4 mr-8"> <div class="flex flex-wrap gap-x-8 gap-y-2 mt-4 mr-8">
{#if trail.location} {#if trail.location}
<h3 class="text-lg"> <h3 class="text-lg">

View File

@@ -8,6 +8,7 @@
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import ShareInfo from "../share_info.svelte"; import ShareInfo from "../share_info.svelte";
import { pb } from "$lib/pocketbase";
export let trail: Trail; export let trail: Trail;
@@ -29,7 +30,7 @@
<h4 class="font-semibold text-lg"> <h4 class="font-semibold text-lg">
{trail.name} {trail.name}
</h4> </h4>
{#if trail.public} {#if trail.public && pb.authStore.model}
<span class="tooltip ml-3" data-title={$_("public")}> <span class="tooltip ml-3" data-title={$_("public")}>
<i class="fa fa-globe"></i> <i class="fa fa-globe"></i>
</span> </span>
@@ -48,6 +49,21 @@
})} })}
</p> </p>
{/if} {/if}
{#if trail.expand.author}
<p class="text-xs text-gray-500 mb-3">
{$_("by")}
<img
class="rounded-full w-5 aspect-square mx-1 inline"
src={getFileURL(
trail.expand.author,
trail.expand.author.avatar,
) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
{trail.expand.author.username}
</p>
{/if}
<div class="flex flex-wrap gap-x-8"> <div class="flex flex-wrap gap-x-8">
{#if trail.location} {#if trail.location}
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5> <h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>

View File

@@ -19,6 +19,7 @@
import Search, { type SearchItem } from "../base/search.svelte"; import Search, { type SearchItem } from "../base/search.svelte";
import type { SelectItem } from "../base/select.svelte"; import type { SelectItem } from "../base/select.svelte";
import Select from "../base/select.svelte"; import Select from "../base/select.svelte";
import UserSearch from "../user_search.svelte";
let openModal: (() => void) | undefined = undefined; let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined; export let closeModal: (() => void) | undefined = undefined;
@@ -35,8 +36,6 @@
let copyButtonText = $_("copy-link"); let copyButtonText = $_("copy-link");
let searchItems: SearchItem[] = [];
let sharesLoading: boolean = false; let sharesLoading: boolean = false;
const permissionSelectItems: SelectItem[] = [ const permissionSelectItems: SelectItem[] = [
@@ -44,24 +43,6 @@
{ text: $_("edit"), value: "edit" }, { text: $_("edit"), value: "edit" },
]; ];
async function updateUsers(q: string) {
try {
const users: User[] = await users_search(q);
searchItems = users.map((u) => ({
text: u.username!,
value: u,
icon: "user",
}));
} catch (e) {
console.error(e);
show_toast({
type: "error",
icon: "close",
text: "Error during search",
});
}
}
function copyURLToClipboard() { function copyURLToClipboard() {
navigator.clipboard.writeText(window.location.href); navigator.clipboard.writeText(window.location.href);
@@ -70,7 +51,6 @@
} }
function close() { function close() {
searchItems = [];
dispatch("save"); dispatch("save");
closeModal!(); closeModal!();
} }
@@ -81,7 +61,10 @@
fetchShares(); fetchShares();
} }
async function updateSharePermission(share:TrailShare, permission: "view"| "edit" ) { async function updateSharePermission(
share: TrailShare,
permission: "view" | "edit",
) {
share.permission = permission; share.permission = permission;
await trail_share_update(share); await trail_share_update(share);
} }
@@ -93,39 +76,26 @@
async function fetchShares() { async function fetchShares() {
sharesLoading = true; sharesLoading = true;
await trail_share_index({trail: trail.id!}); await trail_share_index({ trail: trail.id! });
sharesLoading = false; sharesLoading = false;
} }
</script> </script>
<Modal <Modal
id="share-modal" id="share-modal"
title={$_('share-this-trail')} title={$_("share-this-trail")}
size="max-w-sm overflow-visible" size="max-w-sm overflow-visible"
bind:openModal bind:openModal
bind:closeModal bind:closeModal
> >
<div slot="content"> <div slot="content">
<Search <UserSearch includeSelf={false} on:click={(e) => shareTrail(e.detail)}
on:update={(e) => updateUsers(e.detail)} ></UserSearch>
on:click={(e) => shareTrail(e.detail)} <h4 class="font-semibold mt-4">{$_("shared-with")}</h4>
placeholder={`${$_("username")}`}
items={searchItems}
>
<img
slot="item-header"
let:item
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(item.value, item.value.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${item.value.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
</Search>
<h4 class="font-semibold mt-4">{$_('shared-with')}</h4>
{#if $shares.length == 0} {#if $shares.length == 0}
<p class="text-gray-500 text-center mt-2 text-sm"> <p class="text-gray-500 text-center mt-2 text-sm">
{$_('trail-not-shared')} {$_("trail-not-shared")}
</p> </p>
{:else} {:else}
{#each $shares as share} {#each $shares as share}
@@ -141,17 +111,22 @@
alt="avatar" alt="avatar"
/> />
<p>{share.expand.user.username}</p> <p>{share.expand.user.username}</p>
<span class="basis-full text-sm text-center text-gray-500">{$_('can')}</span> <span
class="basis-full text-sm text-center text-gray-500"
>{$_("can")}</span
>
<div class="shrink-0"> <div class="shrink-0">
<Select <Select
bind:value={share.permission} bind:value={share.permission}
items={permissionSelectItems} items={permissionSelectItems}
on:change={(e) => updateSharePermission(share, e.detail)} on:change={(e) =>
updateSharePermission(share, e.detail)}
></Select> ></Select>
</div> </div>
<button class="btn-icon text-red-500" <button
on:click={() => deleteShare(share)} class="btn-icon text-red-500"
on:click={() => deleteShare(share)}
><i class="fa fa-trash"></i></button ><i class="fa fa-trash"></i></button
> >
</div> </div>

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import type { User } from "$lib/models/user";
import { show_toast } from "$lib/stores/toast_store";
import { users_search } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { createEventDispatcher, tick } from "svelte";
import Search, { type SearchItem } from "./base/search.svelte";
import { _ } from "svelte-i18n";
export let label: string = "";
export let value: string = "";
export let includeSelf: boolean = true;
export let clearAfterSelect: boolean = true;
let searchItems: SearchItem[] = [];
const dispatch = createEventDispatcher();
async function updateUsers(q: string) {
if (!q.length) {
searchItems = [];
dispatch("clear");
return;
}
try {
const users: User[] = await users_search(q, includeSelf);
searchItems = users.map((u) => ({
text: u.username!,
value: u,
icon: "user",
}));
} catch (e) {
console.error(e);
show_toast({
type: "error",
icon: "close",
text: "Error during search",
});
}
}
function onClick(e: CustomEvent<SearchItem>) {
value = e.detail.value.username ?? value;
dispatch("click", e.detail);
searchItems = [];
}
</script>
<Search
on:update={(e) => updateUsers(e.detail)}
on:click={(e) => onClick(e)}
placeholder={`${$_("username")}...`}
items={searchItems}
{clearAfterSelect}
{label}
bind:value
>
<img
slot="item-header"
let:item
class="rounded-full w-8 aspect-square mr-2"
src={getFileURL(item.value, item.value.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${item.value.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
</Search>

View File

@@ -18,10 +18,12 @@
"already-account": "Du hast bereits ein Konto?", "already-account": "Du hast bereits ein Konto?",
"altitude": "Höhe", "altitude": "Höhe",
"api-documentation": "API Dokumentation", "api-documentation": "API Dokumentation",
"author": "",
"avatar": "Avatar", "avatar": "Avatar",
"average-speed": "Durchschn. Geschwindigkeit", "average-speed": "Durchschn. Geschwindigkeit",
"basic-info": "Basisinformation", "basic-info": "Basisinformation",
"before": "Vor", "before": "Vor",
"by": "",
"can": "kann", "can": "kann",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"card": "{n, plural, =1 {Karte} other {Karten}}", "card": "{n, plural, =1 {Karte} other {Karten}}",
@@ -93,6 +95,7 @@
"file-format": "Dateiformat", "file-format": "Dateiformat",
"file-too-big": "", "file-too-big": "",
"filter-categories": "Kategorien filtern", "filter-categories": "Kategorien filtern",
"filter-difficulty": "",
"focus-map-on": "Karte fokussieren auf", "focus-map-on": "Karte fokussieren auf",
"french": "Französisch", "french": "Französisch",
"german": "Deutsch", "german": "Deutsch",
@@ -182,6 +185,7 @@
"share": "Teilen", "share": "Teilen",
"share-this-list": "Diese Liste teilen", "share-this-list": "Diese Liste teilen",
"share-this-trail": "Diese Route teilen", "share-this-trail": "Diese Route teilen",
"shared": "",
"shared-by": "Geteilt von", "shared-by": "Geteilt von",
"shared-with": "Geteilt mit", "shared-with": "Geteilt mit",
"show-in-overview": "In der Übersicht anzeigen", "show-in-overview": "In der Übersicht anzeigen",

View File

@@ -18,10 +18,12 @@
"already-account": "Already have an account?", "already-account": "Already have an account?",
"altitude": "Altitude", "altitude": "Altitude",
"api-documentation": "API Documentation", "api-documentation": "API Documentation",
"author": "Author",
"avatar": "Avatar", "avatar": "Avatar",
"average-speed": "Avg. Speed", "average-speed": "Avg. Speed",
"basic-info": "Basic Info", "basic-info": "Basic Info",
"before": "Before", "before": "Before",
"by": "by",
"can": "can", "can": "can",
"cancel": "Cancel", "cancel": "Cancel",
"card": "{n, plural, =1 {Card} other {Cards}}", "card": "{n, plural, =1 {Card} other {Cards}}",
@@ -93,6 +95,7 @@
"file-format": "File format", "file-format": "File format",
"file-too-big": "File {file} is too big (max. {size})", "file-too-big": "File {file} is too big (max. {size})",
"filter-categories": "Filter categories", "filter-categories": "Filter categories",
"filter-difficulty": "Filter difficulty",
"focus-map-on": "Focus map on", "focus-map-on": "Focus map on",
"french": "French", "french": "French",
"german": "German", "german": "German",
@@ -182,6 +185,7 @@
"share": "Share", "share": "Share",
"share-this-list": "Share this list", "share-this-list": "Share this list",
"share-this-trail": "Share this trail", "share-this-trail": "Share this trail",
"shared": "Shared",
"shared-by": "Shared by", "shared-by": "Shared by",
"shared-with": "Shared with", "shared-with": "Shared with",
"show-in-overview": "Show in overview", "show-in-overview": "Show in overview",

View File

@@ -18,10 +18,12 @@
"already-account": "Déjà un compte ?", "already-account": "Déjà un compte ?",
"altitude": "Altitude", "altitude": "Altitude",
"api-documentation": "Documentation API", "api-documentation": "Documentation API",
"author": "",
"avatar": "Avatar", "avatar": "Avatar",
"average-speed": "", "average-speed": "",
"basic-info": "Informations de base", "basic-info": "Informations de base",
"before": "", "before": "",
"by": "",
"can": "", "can": "",
"cancel": "Annuler", "cancel": "Annuler",
"card": "{n, plural, =1 {Carte} other {Cartes}}", "card": "{n, plural, =1 {Carte} other {Cartes}}",
@@ -93,6 +95,7 @@
"file-format": "", "file-format": "",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "Français", "french": "Français",
"german": "Allemand", "german": "Allemand",
@@ -182,6 +185,7 @@
"share": "", "share": "",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "", "share-this-trail": "",
"shared": "",
"shared-by": "", "shared-by": "",
"shared-with": "", "shared-with": "",
"show-in-overview": "Voir dans l'aperçu", "show-in-overview": "Voir dans l'aperçu",

View File

@@ -18,10 +18,12 @@
"already-account": "Már rendelkezik fiókkal?", "already-account": "Már rendelkezik fiókkal?",
"altitude": "Magasság", "altitude": "Magasság",
"api-documentation": "API Dokumentáció", "api-documentation": "API Dokumentáció",
"author": "",
"avatar": "Avatar", "avatar": "Avatar",
"average-speed": "", "average-speed": "",
"basic-info": "Alap információk", "basic-info": "Alap információk",
"before": "", "before": "",
"by": "",
"can": "", "can": "",
"cancel": "Mégsem", "cancel": "Mégsem",
"card": "{n, plural, =1 {Kártya} other {Kártyák}}", "card": "{n, plural, =1 {Kártya} other {Kártyák}}",
@@ -93,6 +95,7 @@
"file-format": "", "file-format": "",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "Francia", "french": "Francia",
"german": "Német", "german": "Német",
@@ -182,6 +185,7 @@
"share": "", "share": "",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "", "share-this-trail": "",
"shared": "",
"shared-by": "", "shared-by": "",
"shared-with": "", "shared-with": "",
"show-in-overview": "Áttekintés", "show-in-overview": "Áttekintés",

View File

@@ -18,10 +18,12 @@
"already-account": "Hai già un account?", "already-account": "Hai già un account?",
"altitude": "Altitudine", "altitude": "Altitudine",
"api-documentation": "Documentazione API", "api-documentation": "Documentazione API",
"author": "",
"avatar": "Avatar", "avatar": "Avatar",
"average-speed": "", "average-speed": "",
"basic-info": "Informazioni di base", "basic-info": "Informazioni di base",
"before": "", "before": "",
"by": "",
"can": "può", "can": "può",
"cancel": "Annulla", "cancel": "Annulla",
"card": "{n, plural, =1 {Carta} other {Carte}}", "card": "{n, plural, =1 {Carta} other {Carte}}",
@@ -93,6 +95,7 @@
"file-format": "Formato del file", "file-format": "Formato del file",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "Francese", "french": "Francese",
"german": "Tedesco", "german": "Tedesco",
@@ -182,6 +185,7 @@
"share": "Condividi", "share": "Condividi",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "Condividi questo percorso", "share-this-trail": "Condividi questo percorso",
"shared": "",
"shared-by": "Condiviso da", "shared-by": "Condiviso da",
"shared-with": "Condiviso con", "shared-with": "Condiviso con",
"show-in-overview": "Mostra nella panoramica", "show-in-overview": "Mostra nella panoramica",

View File

@@ -18,10 +18,12 @@
"already-account": "Heb je al een account?", "already-account": "Heb je al een account?",
"altitude": "Hoogte", "altitude": "Hoogte",
"api-documentation": "API-documentatie", "api-documentation": "API-documentatie",
"author": "",
"avatar": "Profielfoto", "avatar": "Profielfoto",
"average-speed": "", "average-speed": "",
"basic-info": "Algemene informatie", "basic-info": "Algemene informatie",
"before": "", "before": "",
"by": "",
"can": "", "can": "",
"cancel": "Annuleren", "cancel": "Annuleren",
"card": "{n, plural, =1 {kaart} other {kaarten}}", "card": "{n, plural, =1 {kaart} other {kaarten}}",
@@ -93,6 +95,7 @@
"file-format": "", "file-format": "",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "Frans", "french": "Frans",
"german": "Duits", "german": "Duits",
@@ -182,6 +185,7 @@
"share": "", "share": "",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "", "share-this-trail": "",
"shared": "",
"shared-by": "", "shared-by": "",
"shared-with": "", "shared-with": "",
"show-in-overview": "Tonen op overzicht", "show-in-overview": "Tonen op overzicht",

View File

@@ -18,10 +18,12 @@
"already-account": "Czy masz już konto?", "already-account": "Czy masz już konto?",
"altitude": "Wysokość", "altitude": "Wysokość",
"api-documentation": "Dokumentacja API", "api-documentation": "Dokumentacja API",
"author": "",
"avatar": "Awatar", "avatar": "Awatar",
"average-speed": "", "average-speed": "",
"basic-info": "Podstawowe informacje", "basic-info": "Podstawowe informacje",
"before": "", "before": "",
"by": "",
"can": "", "can": "",
"cancel": "Anuluj", "cancel": "Anuluj",
"card": "{n, plural, =1 {Karta} other {Karty}}", "card": "{n, plural, =1 {Karta} other {Karty}}",
@@ -93,6 +95,7 @@
"file-format": "", "file-format": "",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "Francuski", "french": "Francuski",
"german": "Niemiecki", "german": "Niemiecki",
@@ -182,6 +185,7 @@
"share": "", "share": "",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "", "share-this-trail": "",
"shared": "",
"shared-by": "", "shared-by": "",
"shared-with": "", "shared-with": "",
"show-in-overview": "Pokaż w przeglądzie", "show-in-overview": "Pokaż w przeglądzie",

View File

@@ -18,10 +18,12 @@
"already-account": "Já tem uma conta?", "already-account": "Já tem uma conta?",
"altitude": "Altitude", "altitude": "Altitude",
"api-documentation": "Documentação da API", "api-documentation": "Documentação da API",
"author": "",
"avatar": "Avatar", "avatar": "Avatar",
"average-speed": "", "average-speed": "",
"basic-info": "Informações básicas", "basic-info": "Informações básicas",
"before": "", "before": "",
"by": "",
"can": "", "can": "",
"cancel": "Cancelar", "cancel": "Cancelar",
"card": "{n, plural, =1 {Cartão} other {cartãos}}", "card": "{n, plural, =1 {Cartão} other {cartãos}}",
@@ -93,6 +95,7 @@
"file-format": "", "file-format": "",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "Francês", "french": "Francês",
"german": "Alemão", "german": "Alemão",
@@ -182,6 +185,7 @@
"share": "", "share": "",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "", "share-this-trail": "",
"shared": "",
"shared-by": "", "shared-by": "",
"shared-with": "", "shared-with": "",
"show-in-overview": "Mostrar na vista geral", "show-in-overview": "Mostrar na vista geral",

View File

@@ -18,10 +18,12 @@
"already-account": "已注册账户?", "already-account": "已注册账户?",
"altitude": "海拔", "altitude": "海拔",
"api-documentation": "API 文档", "api-documentation": "API 文档",
"author": "",
"avatar": "头像", "avatar": "头像",
"average-speed": "", "average-speed": "",
"basic-info": "基本信息", "basic-info": "基本信息",
"before": "", "before": "",
"by": "",
"can": "", "can": "",
"cancel": "取消", "cancel": "取消",
"card": "{n, plural, =1 {卡片} other {卡片}}", "card": "{n, plural, =1 {卡片} other {卡片}}",
@@ -93,6 +95,7 @@
"file-format": "", "file-format": "",
"file-too-big": "", "file-too-big": "",
"filter-categories": "", "filter-categories": "",
"filter-difficulty": "",
"focus-map-on": "", "focus-map-on": "",
"french": "法语", "french": "法语",
"german": "德语", "german": "德语",
@@ -182,6 +185,7 @@
"share": "", "share": "",
"share-this-list": "", "share-this-list": "",
"share-this-trail": "", "share-this-trail": "",
"shared": "",
"shared-by": "", "shared-by": "",
"shared-with": "", "shared-with": "",
"show-in-overview": "详情中展示", "show-in-overview": "详情中展示",

View File

@@ -2,6 +2,7 @@ import type { Category } from "./category";
import type { Comment } from "./comment"; import type { Comment } from "./comment";
import type { SummitLog } from "./summit_log"; import type { SummitLog } from "./summit_log";
import type { TrailShare } from "./trail_share"; import type { TrailShare } from "./trail_share";
import type { UserAnonymous } from "./user";
import type { Waypoint } from "./waypoint"; import type { Waypoint } from "./waypoint";
class Trail { class Trail {
@@ -28,6 +29,7 @@ class Trail {
category?: Category; category?: Category;
waypoints: Waypoint[] waypoints: Waypoint[]
summit_logs: SummitLog[] summit_logs: SummitLog[]
author?: UserAnonymous
comments_via_trail: Comment[] comments_via_trail: Comment[]
gpx_data?: string gpx_data?: string
trail_share_via_trail?: TrailShare[] trail_share_via_trail?: TrailShare[]
@@ -97,6 +99,9 @@ interface TrailFilter {
q: string, q: string,
category: string[], category: string[],
difficulty: ("easy" | "moderate" | "difficult")[] difficulty: ("easy" | "moderate" | "difficult")[]
author?: string;
public?: boolean;
shared?: boolean;
near: { near: {
lat?: number, lat?: number,
lon?: number, lon?: number,
@@ -138,4 +143,4 @@ interface TrailBoundingBox {
export { Trail }; export { Trail };
export type { TrailFilter, TrailFilterValues, TrailBoundingBox }; export type { TrailBoundingBox, TrailFilter, TrailFilterValues };

View File

@@ -8,4 +8,10 @@ export type User = {
avatar?: string; avatar?: string;
language?: string; language?: string;
created?: string; created?: string;
}
export type UserAnonymous = {
id: string,
username?: string,
avatar?: string;
} }

View File

@@ -401,7 +401,35 @@ function buildFilterText(filter: TrailFilter, includeGeo: boolean): string {
filterText += ` AND elevation_loss <= ${Math.ceil(filter.elevationLossMax)}` filterText += ` AND elevation_loss <= ${Math.ceil(filter.elevationLossMax)}`
} }
filterText += ` AND difficulty IN [${filter.difficulty.join(",")}]` if (filter.difficulty.length > 0) {
filterText += ` AND difficulty IN [${filter.difficulty.join(",")}]`
}
if (filter.author?.length) {
filterText += ` AND author = ${filter.author}`
}
if (filter.public !== undefined || filter.shared !== undefined) {
filterText += " AND ("
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 += ")"
}
if (filter.startDate) { if (filter.startDate) {
filterText += ` AND date >= ${new Date(filter.startDate).getTime() / 1000}` filterText += ` AND date >= ${new Date(filter.startDate).getTime() / 1000}`

View File

@@ -20,9 +20,9 @@ export async function users_create(user: User) {
return createdUser; return createdUser;
} }
export async function users_search(q: string) { export async function users_search(q: string, includeSelf: boolean = true) {
let r = await fetch('/api/v1/user/anonymous?' + new URLSearchParams({ let r = await fetch('/api/v1/user/anonymous?' + new URLSearchParams({
"filter": `username~"${q}"&&id!="${pb.authStore.model?.id}"`, "filter": `username~"${q}"${includeSelf ? '' : '&&id!="${pb.authStore.model?.id}"'}`,
}), { }), {
method: 'GET', method: 'GET',
}) })

View File

@@ -21,8 +21,18 @@ export async function GET(event: RequestEvent) {
.getList<Trail>(parseInt(page), parseInt(perPage), { expand: expand ?? "", sort: sort ?? "", filter: filter ?? "" }) .getList<Trail>(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 = {} as any
}
t.expand.author = await pb.collection("users_anonymous").getOne(t.author);
}
return json(r) return json(r)
} catch (e: any) { } catch (e: any) {
console.error(e)
throw error(e.status, e); throw error(e.status, e);
} }
} }

View File

@@ -9,14 +9,22 @@ export async function GET(event: RequestEvent) {
const r = await pb.collection('trails') const r = await pb.collection('trails')
.getOne<Trail>(event.params.id as string, { expand: expand ?? "" }) .getOne<Trail>(event.params.id as string, { expand: expand ?? "" })
if (pb.authStore.model) {
if (!r.expand) {
r.expand = {} as any
}
r.expand.author = await pb.collection("users_anonymous").getOne(r.author!);
}
// remove time from dates // remove time from dates
r.date = r.date?.substring(0, 10) ?? "" r.date = r.date?.substring(0, 10) ?? ""
for (const log of r.expand?.summit_logs ?? []) { for (const log of r.expand?.summit_logs ?? []) {
log.date = log.date.substring(0, 10) log.date = log.date.substring(0, 10)
} }
return json(r) return json(r)
} catch (e: any) { } catch (e: any) {
throw error(e.status || 500, e); throw error(e.status || 500, e);
} }
} }

View File

@@ -11,6 +11,9 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
q: "", q: "",
category: [], category: [],
difficulty: ["easy", "moderate", "difficult"], difficulty: ["easy", "moderate", "difficult"],
author: "",
public: true,
shared: true,
near: { near: {
radius: 2000, radius: 2000,
}, },

View File

@@ -10,6 +10,9 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
q: "", q: "",
category: [], category: [],
difficulty: ["easy", "moderate", "difficult"], difficulty: ["easy", "moderate", "difficult"],
author: "",
public: true,
shared: true,
near: { near: {
radius: 2000, radius: 2000,
}, },

Binary file not shown.