adds trail author filter
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
67
web/src/lib/components/user_search.svelte
Normal file
67
web/src/lib/components/user_search.svelte
Normal 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>
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "详情中展示",
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -9,3 +9,9 @@ export type User = {
|
|||||||
language?: string;
|
language?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserAnonymous = {
|
||||||
|
id: string,
|
||||||
|
username?: string,
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ 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 ?? []) {
|
||||||
@@ -16,7 +24,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
}
|
}
|
||||||
return json(r)
|
return json(r)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw error(e.status || 500, e);
|
throw error(e.status || 500, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
web/static/models/mountain.glb
Normal file
BIN
web/static/models/mountain.glb
Normal file
Binary file not shown.
Reference in New Issue
Block a user