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

@@ -8,6 +8,7 @@
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import ShareInfo from "../share_info.svelte";
import { pb } from "$lib/pocketbase";
export let trail: Trail;
@@ -27,15 +28,15 @@
<div
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>
{#if trail.public || trailIsShared}
{#if (trail.public || trailIsShared) && pb.authStore.model}
<div
class="flex absolute top-4 right-4 {trail.public && trailIsShared
? 'w-14'
: 'w-8'} h-8 rounded-full items-center justify-center bg-background text-content"
>
{#if trail.public}
{#if trail.public && pb.authStore.model}
<span
class="tooltip"
class:mr-2={trail.public && trailIsShared}
@@ -62,6 +63,20 @@
})}
</p>
{/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">
{#if trail.location}
<h5>
@@ -75,7 +90,9 @@
</h5>
</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
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance,
@@ -101,12 +118,12 @@
</div>
<style>
.trail-card img {
.trail-card #header-img {
object-fit: cover;
transition: 0.25s ease;
}
.trail-card:hover img {
.trail-card:hover #header-img {
scale: 1.075;
}
</style>

View File

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

View File

@@ -38,6 +38,7 @@
import PhotoGallery from "../photo_gallery.svelte";
import ShareInfo from "../share_info.svelte";
import SummitLogTable from "../summit_log/summit_log_table.svelte";
import { pb } from "$lib/pocketbase";
export let trail: Trail;
export let mode: "overview" | "map" | "list" = "map";
@@ -184,14 +185,14 @@
<div
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
></div>
{#if trail.public || trailIsShared}
{#if (trail.public || trailIsShared) && pb.authStore.model}
<div
class="flex absolute top-8 right-6 {trail.public &&
trailIsShared
? 'w-16'
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
>
{#if trail.public}
{#if trail.public && pb.authStore.model}
<span
class="tooltip text-2xl"
class:mr-3={trail.public && trailIsShared}
@@ -226,6 +227,21 @@
)}
</h5>
{/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">
{#if trail.location}
<h3 class="text-lg">

View File

@@ -8,6 +8,7 @@
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import ShareInfo from "../share_info.svelte";
import { pb } from "$lib/pocketbase";
export let trail: Trail;
@@ -29,7 +30,7 @@
<h4 class="font-semibold text-lg">
{trail.name}
</h4>
{#if trail.public}
{#if trail.public && pb.authStore.model}
<span class="tooltip ml-3" data-title={$_("public")}>
<i class="fa fa-globe"></i>
</span>
@@ -48,6 +49,21 @@
})}
</p>
{/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">
{#if trail.location}
<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 type { SelectItem } from "../base/select.svelte";
import Select from "../base/select.svelte";
import UserSearch from "../user_search.svelte";
let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
@@ -35,8 +36,6 @@
let copyButtonText = $_("copy-link");
let searchItems: SearchItem[] = [];
let sharesLoading: boolean = false;
const permissionSelectItems: SelectItem[] = [
@@ -44,24 +43,6 @@
{ 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() {
navigator.clipboard.writeText(window.location.href);
@@ -70,7 +51,6 @@
}
function close() {
searchItems = [];
dispatch("save");
closeModal!();
}
@@ -81,7 +61,10 @@
fetchShares();
}
async function updateSharePermission(share:TrailShare, permission: "view"| "edit" ) {
async function updateSharePermission(
share: TrailShare,
permission: "view" | "edit",
) {
share.permission = permission;
await trail_share_update(share);
}
@@ -93,39 +76,26 @@
async function fetchShares() {
sharesLoading = true;
await trail_share_index({trail: trail.id!});
await trail_share_index({ trail: trail.id! });
sharesLoading = false;
}
</script>
<Modal
id="share-modal"
title={$_('share-this-trail')}
title={$_("share-this-trail")}
size="max-w-sm overflow-visible"
bind:openModal
bind:closeModal
>
<div slot="content">
<Search
on:update={(e) => updateUsers(e.detail)}
on:click={(e) => shareTrail(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>
<UserSearch includeSelf={false} on:click={(e) => shareTrail(e.detail)}
></UserSearch>
<h4 class="font-semibold mt-4">{$_("shared-with")}</h4>
{#if $shares.length == 0}
<p class="text-gray-500 text-center mt-2 text-sm">
{$_('trail-not-shared')}
{$_("trail-not-shared")}
</p>
{:else}
{#each $shares as share}
@@ -141,17 +111,22 @@
alt="avatar"
/>
<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">
<Select
bind:value={share.permission}
items={permissionSelectItems}
on:change={(e) => updateSharePermission(share, e.detail)}
on:change={(e) =>
updateSharePermission(share, e.detail)}
></Select>
</div>
<button class="btn-icon text-red-500"
on:click={() => deleteShare(share)}
<button
class="btn-icon text-red-500"
on:click={() => deleteShare(share)}
><i class="fa fa-trash"></i></button
>
</div>