Merge remote-tracking branch 'origin/main' into brian/trail-card-tags
This commit is contained in:
2417
web/package-lock.json
generated
2417
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wanderer",
|
||||
"version": "0.16.5",
|
||||
"version": "0.17.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -17,12 +17,15 @@
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^20.11.25",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"postcss": "^8.4.33",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^5.4.4",
|
||||
@@ -35,11 +38,22 @@
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@threlte/core": "^8.0.0-next.41",
|
||||
"@threlte/extras": "^9.0.0-next.55",
|
||||
"@tiptap/core": "^2.14.0",
|
||||
"@tiptap/extension-heading": "^2.14.0",
|
||||
"@tiptap/extension-link": "^2.14.0",
|
||||
"@tiptap/extension-mention": "^2.14.0",
|
||||
"@tiptap/extension-placeholder": "^2.14.0",
|
||||
"@tiptap/extension-underline": "^2.14.0",
|
||||
"@tiptap/pm": "^2.14.0",
|
||||
"@tiptap/starter-kit": "^2.14.0",
|
||||
"@tiptap/suggestion": "^2.14.0",
|
||||
"@turf/destination": "^7.1.0",
|
||||
"@turf/distance": "^7.1.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/three": "^0.161.2",
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"activitypub-types": "^1.1.0",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"canvg": "^4.0.1",
|
||||
"chart.js": "^4.4.6",
|
||||
"chartjs-plugin-crosshair": "^2.0.0",
|
||||
@@ -49,7 +63,6 @@
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
"isomorphic-xml2js": "^0.1.3",
|
||||
"js-confetti": "^0.12.0",
|
||||
"jspdf": "^2.5.1",
|
||||
"jszip": "^3.10.1",
|
||||
"maplibre-gl": "^4.7.1",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,49 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/preflight.css" layer(base);
|
||||
@import "tailwindcss/utilities.css";
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-primary: rgba(var(--primary));
|
||||
--color-background: rgba(var(--background));
|
||||
--color-background-inverse: rgba(var(--background-inverse));
|
||||
--color-content: rgba(var(--content));
|
||||
--color-content-inverse: rgba(var(--content-inverse));
|
||||
|
||||
--color-primary: rgba(var(--primary));
|
||||
--color-primary-hover: rgba(var(--primary-hover));
|
||||
--color-secondary-hover: rgba(var(--secondary-hover));
|
||||
--color-input-background: rgba(var(--input-background));
|
||||
--color-input-background-error: rgba(var(--input-background-error));
|
||||
--color-input-border: rgba(var(--input-border));
|
||||
--color-input-border-focus: rgba(var(--input-border-focus));
|
||||
--color-input-ring: rgba(var(--input-ring));
|
||||
--color-menu-background: rgba(var(--menu-background));
|
||||
--color-menu-item-background-hover: rgba(var(--menu-item-background-hover));
|
||||
--color-menu-item-background-focus: rgba(var(--menu-item-background-focus));
|
||||
--color-footer-background: rgba(var(--footer-background));
|
||||
--color-separator: rgba(var(--separator));
|
||||
|
||||
--font-sans: "IBMPlexSans"
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
dialog {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IBMPlexSans";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss';
|
||||
@reference "./app.css";
|
||||
|
||||
.btn-primary {
|
||||
@apply min-h-10 text-white rounded-lg px-4 py-2 bg-primary font-semibold transition-all hover:bg-primary-hover focus:ring-4 ring-input-ring
|
||||
@@ -125,4 +124,9 @@
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mention {
|
||||
@apply bg-blue-100 dark:bg-slate-700 rounded-md;
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
--input-border: 209, 213, 219;
|
||||
--input-border-focus: 36, 39, 52;
|
||||
--input-ring: 148, 163, 184;
|
||||
--input-ring-inverse: 108, 115, 150;
|
||||
|
||||
--menu-background: 255, 255, 255;
|
||||
--menu-item-background-hover: 243, 244, 246;
|
||||
@@ -31,28 +30,29 @@
|
||||
|
||||
.dark {
|
||||
--primary: 36, 39, 52;
|
||||
--primary-hover: 44, 48, 64;
|
||||
--secondary-hover: 36, 39, 52;
|
||||
--primary-hover: 55, 60, 80;
|
||||
--secondary-hover: 55, 65, 81;
|
||||
--secondary-hover-inverse: 243, 244, 246;
|
||||
|
||||
--background: 7, 10, 36;
|
||||
--background: 18, 20, 28;
|
||||
--background-inverse: 255, 255, 255;
|
||||
|
||||
--content: 255, 255, 255;
|
||||
--content-inverse: 0, 0, 0;
|
||||
|
||||
--input-background: 36, 39, 52;
|
||||
--input-background-error: 69, 10, 10;
|
||||
--input-border: 43, 46, 61;
|
||||
--input-border-focus: 108, 115, 150;
|
||||
--input-ring: 108, 115, 150;
|
||||
--input-background: 38, 40, 49;
|
||||
--input-background-error: 76, 17, 17;
|
||||
--input-border: 50, 54, 64;
|
||||
--input-border-focus: 148, 163, 184;
|
||||
--input-ring: 148, 163, 184;
|
||||
|
||||
--menu-background: 33, 36, 54;
|
||||
--menu-item-background-hover: 43, 46, 69;
|
||||
--menu-item-background-focus: 25, 26, 41;
|
||||
--menu-background: 30, 32, 40;
|
||||
--menu-item-background-hover: 55, 65, 81;
|
||||
--menu-item-background-focus: 75, 85, 99;
|
||||
|
||||
--footer-background: 26, 29, 51;
|
||||
--footer-background: 24, 26, 36;
|
||||
|
||||
--separator: 43, 46, 61;
|
||||
--separator: 55, 65, 81;
|
||||
|
||||
background: rgba(var(--background));
|
||||
color: rgba(var(--content));
|
||||
|
||||
@@ -8,6 +8,7 @@ import { json, redirect, text, type Handle } from '@sveltejs/kit'
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
import { MeiliSearch } from 'meilisearch'
|
||||
import { locale } from 'svelte-i18n'
|
||||
import type { Actor } from '$lib/models/activitypub/actor'
|
||||
|
||||
|
||||
function csrf(allowedPaths: string[]): Handle {
|
||||
@@ -77,9 +78,11 @@ const auth: Handle = async ({ event, resolve }) => {
|
||||
|
||||
let meiliApiKey: string = "";
|
||||
let settings: Settings | undefined;
|
||||
let actor: Actor | undefined;
|
||||
if (pb.authStore.record) {
|
||||
meiliApiKey = pb.authStore.record.token
|
||||
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.record.id}"`, { requestKey: null })
|
||||
actor = await pb.collection("activitypub_actors").getFirstListItem(`user='${pb.authStore.record.id}'`)
|
||||
} else {
|
||||
const response = await pb.send("/public/search/token", { method: "GET", fetch: event.fetch });
|
||||
meiliApiKey = response.token;
|
||||
@@ -89,6 +92,9 @@ const auth: Handle = async ({ event, resolve }) => {
|
||||
event.locals.ms = ms
|
||||
event.locals.pb = pb
|
||||
event.locals.user = pb.authStore.record
|
||||
if (event.locals.user) {
|
||||
event.locals.user.actor = actor?.id
|
||||
}
|
||||
event.locals.settings = settings
|
||||
|
||||
const lang = settings?.language ?? event.request.headers.get('accept-language')?.split(',')[0]
|
||||
|
||||
77
web/src/lib/components/actor_search.svelte
Normal file
77
web/src/lib/components/actor_search.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import { searchActors } from "$lib/stores/search_store";
|
||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import Search, { type SearchItem } from "./base/search.svelte";
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
value?: string;
|
||||
includeSelf?: boolean;
|
||||
clearAfterSelect?: boolean;
|
||||
onclear?: () => void;
|
||||
onclick?: (item: SearchItem) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
label = "",
|
||||
value = $bindable(""),
|
||||
includeSelf = true,
|
||||
clearAfterSelect = true,
|
||||
onclear,
|
||||
onclick,
|
||||
}: Props = $props();
|
||||
|
||||
let searchItems: SearchItem[] = $state([]);
|
||||
|
||||
async function updateActors(q: string) {
|
||||
if (!q.length) {
|
||||
searchItems = [];
|
||||
onclear?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const actors: Actor[] = await searchActors(q, includeSelf);
|
||||
searchItems = actors.map((a) => ({
|
||||
text: "a.username!",
|
||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
||||
value: a,
|
||||
icon: "user",
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error during search",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(item: SearchItem) {
|
||||
value = item.value.preferred_username ?? value;
|
||||
|
||||
onclick?.(item);
|
||||
searchItems = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Search
|
||||
onupdate={(q) => updateActors(q)}
|
||||
onclick={(item) => onClick(item)}
|
||||
placeholder={`${$_("username")}...`}
|
||||
items={searchItems}
|
||||
{clearAfterSelect}
|
||||
{label}
|
||||
bind:value
|
||||
>
|
||||
{#snippet prepend({ item })}
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mr-2"
|
||||
src={item.value.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${item.value.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{/snippet}
|
||||
</Search>
|
||||
@@ -113,10 +113,7 @@
|
||||
}
|
||||
|
||||
function colorKey(a: typeof currentMonthArray, i: number) {
|
||||
return $_(
|
||||
a[i]?.log?.expand?.trails_via_summit_logs?.at(0)?.expand?.category
|
||||
?.name ?? "",
|
||||
);
|
||||
return $_(a[i]?.log?.expand?.trail?.expand?.category?.name ?? "");
|
||||
}
|
||||
|
||||
function handleDateClick(date?: Date) {
|
||||
@@ -170,7 +167,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
@reference "tailwindcss";
|
||||
@reference "../../../css/app.css";
|
||||
|
||||
.calendar-weekday {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@
|
||||
|
||||
<div class="my-4" id="slider" bind:this={sliderContainer}></div>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
@reference "tailwindcss";
|
||||
@reference "../../../css/app.css";
|
||||
|
||||
:global(.noUi-horizontal) {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="fixed drawer-overlay h-screen w-screen backdrop-blur-md z-50 bg-gray-500/50 cursor-pointer"
|
||||
class="fixed drawer-overlay h-screen w-screen backdrop-blur-md z-50 bg-gray-500 opacity-50 cursor-pointer"
|
||||
onclick={() => (open = false)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key == "Escape") open = false;
|
||||
|
||||
@@ -29,28 +29,50 @@
|
||||
e.preventDefault();
|
||||
|
||||
isOpen = !isOpen;
|
||||
|
||||
if (isOpen) {
|
||||
await tick();
|
||||
const toggleRect = dropdownToggleElement.getBoundingClientRect();
|
||||
|
||||
const toggleRect = dropdownToggleElement.getBoundingClientRect();
|
||||
const dropdownRect = dropdownElement.getBoundingClientRect();
|
||||
dropdownElement.style.visibility = "";
|
||||
|
||||
// Viewport dimensions
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - toggleRect.bottom;
|
||||
const spaceAbove = toggleRect.top;
|
||||
const spaceBelowViewport = viewportHeight - toggleRect.bottom;
|
||||
const spaceAboveViewport = toggleRect.top;
|
||||
|
||||
if (
|
||||
// Container dimensions
|
||||
const scrollContainer =
|
||||
dropdownElement.closest(".scroll-x-only") ?? document.body;
|
||||
const containerRect = scrollContainer.getBoundingClientRect();
|
||||
const spaceBelowContainer =
|
||||
containerRect.bottom - toggleRect.bottom;
|
||||
const spaceAboveContainer = toggleRect.top - containerRect.top;
|
||||
|
||||
// Effective space
|
||||
const spaceBelow = Math.min(
|
||||
spaceBelowViewport,
|
||||
spaceBelowContainer,
|
||||
);
|
||||
const spaceAbove = Math.min(
|
||||
spaceAboveViewport,
|
||||
spaceAboveContainer,
|
||||
);
|
||||
|
||||
// Determine direction
|
||||
const openUpward =
|
||||
spaceBelow < dropdownRect.height &&
|
||||
spaceAbove > dropdownRect.height
|
||||
) {
|
||||
spaceAbove > dropdownRect.height;
|
||||
|
||||
if (openUpward) {
|
||||
dropdownElement.classList.remove("rounded-b-xl");
|
||||
dropdownElement.classList.add("rounded-t-xl");
|
||||
dropdownElement.style.top = `${-8-dropdownRect.height}px`;
|
||||
dropdownElement.style.top = `${-8 - dropdownRect.height}px`;
|
||||
} else {
|
||||
dropdownElement.classList.remove("rounded-t-xl");
|
||||
dropdownElement.classList.add("rounded-b-xl");
|
||||
dropdownElement.style.top = `${toggleRect.height +8}px`;
|
||||
dropdownElement.style.top = `${toggleRect.height + 8}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
501
web/src/lib/components/base/editor.svelte
Normal file
501
web/src/lib/components/base/editor.svelte
Normal file
@@ -0,0 +1,501 @@
|
||||
<script lang="ts">
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import { Link } from "@tiptap/extension-link";
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { Underline } from "@tiptap/extension-underline";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { mount, onDestroy, onMount, unmount, untrack } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { z, ZodError } from "zod";
|
||||
import { type DropdownItem } from "./dropdown.svelte";
|
||||
import Modal from "./modal.svelte";
|
||||
import {
|
||||
default as DropdownList,
|
||||
default as SearchList,
|
||||
} from "./search_list.svelte";
|
||||
import { type SelectItem } from "./select.svelte";
|
||||
import TextField from "./text_field.svelte";
|
||||
import Toggle from "./toggle.svelte";
|
||||
import type { SearchItem } from "./search.svelte";
|
||||
import type { SuggestionProps } from "@tiptap/suggestion";
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import { searchActors } from "$lib/stores/search_store";
|
||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||
|
||||
let element: HTMLElement;
|
||||
let editor: Editor | undefined = $state();
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
interface Props {
|
||||
value?: string;
|
||||
label?: string;
|
||||
error?: string | string[] | null;
|
||||
placeholder?: string;
|
||||
extraClasses?: string;
|
||||
searchListPosition?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(""),
|
||||
label = "",
|
||||
error = "",
|
||||
placeholder = "",
|
||||
extraClasses = "",
|
||||
searchListPosition = "absolute"
|
||||
}: Props = $props();
|
||||
|
||||
const fontSizes: SelectItem[] = [
|
||||
{ text: $_("paragraph"), value: "p" },
|
||||
{ text: `${$_("heading")} 1`, value: "h1" },
|
||||
{ text: `${$_("heading")} 2`, value: "h2" },
|
||||
{ text: `${$_("heading")} 3`, value: "h3" },
|
||||
{ text: `${$_("heading")} 4`, value: "h4" },
|
||||
{ text: `${$_("heading")} 5`, value: "h5" },
|
||||
];
|
||||
|
||||
let currentFontSize: string = $state("p");
|
||||
let linkURL: string = $state("");
|
||||
let linkURLError: string = $state("");
|
||||
let linkText: string = $state("");
|
||||
let openLinkInNewTab: boolean = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
value;
|
||||
untrack(() => {
|
||||
if (value !== editor?.getHTML()) {
|
||||
editor?.commands.setContent(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element: element,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder,
|
||||
}),
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
},
|
||||
renderHTML({ node, options }) {
|
||||
return [
|
||||
"a",
|
||||
mergeAttributes(
|
||||
{ href: `/profile/@${options.HTMLAttributes["data-label"]}` },
|
||||
options.HTMLAttributes,
|
||||
),
|
||||
options.renderText?.({
|
||||
options: options,
|
||||
node,
|
||||
}),
|
||||
];
|
||||
},
|
||||
suggestion: {
|
||||
allowToIncludeChar: true,
|
||||
items: async ({ query }) => {
|
||||
try {
|
||||
const actors: Actor[] = await searchActors(
|
||||
query,
|
||||
true,
|
||||
);
|
||||
return actors.map((a) => ({
|
||||
text: a.username!,
|
||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
||||
value: a,
|
||||
icon:
|
||||
a.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${a.preferred_username}&backgroundType=gradientLinear`,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error during search",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let component: DropdownList;
|
||||
const componentState: any = $state({
|
||||
items: [],
|
||||
id: "mention-list",
|
||||
});
|
||||
|
||||
function updatePosition({
|
||||
clientRect,
|
||||
}: SuggestionProps) {
|
||||
const searchListElement =
|
||||
document.getElementById("mention-list");
|
||||
if (!clientRect || !searchListElement) return;
|
||||
const box = clientRect();
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchListElement.style.position = searchListPosition;
|
||||
searchListElement.style.top = `${box.bottom + window.scrollY + 4}px`;
|
||||
searchListElement.style.left = `${box.left + window.scrollX}px`;
|
||||
searchListElement.style.zIndex = "1001";
|
||||
}
|
||||
|
||||
function onActorClick(props: SuggestionProps) {
|
||||
return (_: Event, item: SearchItem) => {
|
||||
props.command({
|
||||
id: item.value.iri,
|
||||
label: `${item.value.username}${item.value.isLocal ? "" : "@" + item.value.domain}`,
|
||||
});
|
||||
};
|
||||
}
|
||||
return {
|
||||
onStart: (props) => {
|
||||
props.clientRect;
|
||||
componentState.items = props.items;
|
||||
componentState.onclick =
|
||||
onActorClick(props);
|
||||
|
||||
component = mount(SearchList, {
|
||||
target: element,
|
||||
props: componentState,
|
||||
});
|
||||
|
||||
updatePosition(props);
|
||||
},
|
||||
onUpdate(props) {
|
||||
componentState.items = props.items;
|
||||
componentState.onclick =
|
||||
onActorClick(props);
|
||||
updatePosition(props);
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === "Escape") {
|
||||
this.onExit?.({} as unknown as any)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
onExit() {
|
||||
unmount(component);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: "https",
|
||||
protocols: ["http", "https"],
|
||||
isAllowedUri: (url, ctx) => {
|
||||
try {
|
||||
const parsedUrl = url.includes(":")
|
||||
? new URL(url)
|
||||
: new URL(`${ctx.defaultProtocol}://${url}`);
|
||||
|
||||
if (!ctx.defaultValidate(parsedUrl.href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const disallowedProtocols = [
|
||||
"ftp",
|
||||
"file",
|
||||
"mailto",
|
||||
];
|
||||
const protocol = parsedUrl.protocol.replace(
|
||||
":",
|
||||
"",
|
||||
);
|
||||
|
||||
if (disallowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedProtocols = ctx.protocols.map((p) =>
|
||||
typeof p === "string" ? p : p.scheme,
|
||||
);
|
||||
|
||||
if (!allowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: value,
|
||||
onTransaction: ({ editor: newEditor }) => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = undefined;
|
||||
editor = newEditor;
|
||||
},
|
||||
onUpdate: (props) => {
|
||||
value = editor?.getHTML() ?? "";
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: `prose dark:prose-invert text-content bg-input-background border border-input-border rounded-md p-3 resize-none transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 ${extraClasses}`,
|
||||
},
|
||||
},
|
||||
onSelectionUpdate: ({ editor }) => {
|
||||
if (editor.isActive("paragraph")) {
|
||||
currentFontSize = "p";
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (editor.isActive("heading", { level: i })) {
|
||||
currentFontSize = "h" + i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
function openLinkModal() {
|
||||
linkURL = editor?.getAttributes("link").href ?? "";
|
||||
openLinkInNewTab = editor?.getAttributes("link").target === "_blank";
|
||||
if (linkURL) {
|
||||
editor?.chain().focus().extendMarkRange("link").run();
|
||||
}
|
||||
linkText =
|
||||
editor?.state.doc.textBetween(
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to,
|
||||
"",
|
||||
) ?? "";
|
||||
|
||||
modal.openModal();
|
||||
}
|
||||
|
||||
function setLink() {
|
||||
linkURLError = "";
|
||||
|
||||
if (linkURL === "") {
|
||||
linkURLError = $_("required");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
z.string().url().parse(linkURL);
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: "text",
|
||||
text: linkText || linkURL,
|
||||
marks: [
|
||||
{
|
||||
type: "link",
|
||||
attrs: {
|
||||
href: linkURL,
|
||||
target: openLinkInNewTab ? "_blank" : null,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.run();
|
||||
|
||||
modal.closeModal();
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ZodError &&
|
||||
e.errors[0].code === "invalid_string"
|
||||
) {
|
||||
linkURLError = $_("not-a-valid-url");
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function unsetLink() {
|
||||
editor?.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
modal.closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="editor-wrapper">
|
||||
{#if label.length}
|
||||
<p class="text-sm font-medium mb-1">
|
||||
{label}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-wrap items-center py-2 gap-y-2">
|
||||
<div class="mr-2">
|
||||
<!-- <Select
|
||||
items={fontSizes}
|
||||
bind:value={currentFontSize}
|
||||
onchange={(value: string) => {
|
||||
if (value.startsWith("p")) {
|
||||
editor?.chain().focus().setParagraph().run();
|
||||
} else {
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.setHeading({
|
||||
level: parseInt(value.substring(1)) as Level,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}}
|
||||
></Select> -->
|
||||
</div>
|
||||
<div class="flex gap-2 border-r border-input-border pr-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() => editor?.chain().focus().toggleBold().run()}
|
||||
class:ring-2={editor?.isActive("bold")}
|
||||
aria-label="Bold"
|
||||
>
|
||||
<i class="fas fa-bold"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() => editor?.chain().focus().toggleItalic().run()}
|
||||
class:ring-2={editor?.isActive("italic")}
|
||||
aria-label="Italic"
|
||||
>
|
||||
<i class="fas fa-italic"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() => editor?.chain().focus().toggleUnderline().run()}
|
||||
class:ring-2={editor?.isActive("underline")}
|
||||
aria-label="Underline"
|
||||
>
|
||||
<i class="fas fa-underline"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 border-r border-input-border px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() => editor?.chain().focus().toggleBulletList().run()}
|
||||
class:ring-2={editor?.isActive("bulletList")}
|
||||
aria-label="Bullet List"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() =>
|
||||
editor?.chain().focus().toggleOrderedList().run()}
|
||||
class:ring-2={editor?.isActive("orderedList")}
|
||||
aria-label="Numbered List"
|
||||
>
|
||||
<i class="fas fa-list-ol"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 border-r border-input-border px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() => editor?.chain().focus().toggleBlockquote().run()}
|
||||
class:ring-2={editor?.isActive("blockquote")}
|
||||
aria-label="Quote"
|
||||
>
|
||||
<i class="fas fa-quote-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 px-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon"
|
||||
onclick={() => openLinkModal()}
|
||||
class:ring-2={editor?.isActive("link")}
|
||||
aria-label="Link"
|
||||
>
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div bind:this={element}></div>
|
||||
|
||||
{#if error}
|
||||
<span class="editor-error text-xs text-red-400">
|
||||
{error instanceof Array ? $_(error[0]) : error}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Modal
|
||||
id="editor-modal"
|
||||
title={"Insert/edit link"}
|
||||
size="md:min-w-lg"
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet content()}
|
||||
<TextField label={"URL"} bind:value={linkURL} error={linkURLError}
|
||||
></TextField>
|
||||
<TextField label={$_("text")} bind:value={linkText}></TextField>
|
||||
<Toggle
|
||||
label={$_("open-in-new-tab", { values: { n: 2 } })}
|
||||
bind:value={openLinkInNewTab}
|
||||
></Toggle>
|
||||
{/snippet}
|
||||
{#snippet footer()}
|
||||
<div class="flex items-center gap-4">
|
||||
{#if editor?.getAttributes("link").href}
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary shrink-0"
|
||||
onclick={() => unsetLink()}
|
||||
aria-label="Link"
|
||||
>
|
||||
<i class="fas fa-link-slash"></i>
|
||||
<span>{$_("unlink")}</span>
|
||||
</button>
|
||||
<div class="basis-full"></div>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
onclick={() => modal.closeModal()}>{$_("cancel")}</button
|
||||
>
|
||||
<button
|
||||
class="btn-primary"
|
||||
type="button"
|
||||
name="save"
|
||||
onclick={() => setLink()}>{$_("save")}</button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
:global(.ProseMirror p.is-editor-empty:first-child::before) {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #adb5bd;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -42,7 +42,7 @@
|
||||
{id}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
class="w-full {size} max-h-full rounded-xl text-content"
|
||||
class="{size} max-h-full rounded-xl text-content"
|
||||
>
|
||||
<!-- Modal content -->
|
||||
<div class="bg-background shadow rounded-xl">
|
||||
@@ -73,8 +73,9 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
@reference "tailwindcss";
|
||||
dialog::backdrop {
|
||||
@apply bg-gray-500/50;
|
||||
@apply bg-gray-500 opacity-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import TextField from "./text_field.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import SearchList from "./search_list.svelte";
|
||||
import TextField from "./text_field.svelte";
|
||||
|
||||
interface Props {
|
||||
maxSearchLength?: number;
|
||||
@@ -118,34 +119,6 @@
|
||||
></TextField>
|
||||
|
||||
{#if dropDownOpen}
|
||||
<ul
|
||||
class="menu absolute bg-menu-background border border-input-border rounded-xl shadow-md overflow-x-hidden overflow-y-scroll max-h-72 w-full"
|
||||
class:none={!dropDownOpen}
|
||||
style="z-index: 1001"
|
||||
>
|
||||
{#each items as item}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
tabindex="0"
|
||||
onmousedown={(e) => handleItemClick(e, item)}
|
||||
onkeydown={(e) => handleItemClick(e, item)}
|
||||
>
|
||||
{#if prepend}{@render prepend({ item })}{:else}
|
||||
<i class="fa fa-{item.icon} basis-8 shrink-0"></i>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<p>{item.text}</p>
|
||||
{#if item.description}
|
||||
<p class="text-sm text-gray-500">
|
||||
{item.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<SearchList {items} extraClasses="w-full" {prepend} onclick={handleItemClick}></SearchList>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
67
web/src/lib/components/base/search_list.svelte
Normal file
67
web/src/lib/components/base/search_list.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { isURL } from "$lib/util/file_util";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { SearchItem } from "./search.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
interface Props {
|
||||
id?: string;
|
||||
items?: SearchItem[];
|
||||
element?: HTMLUListElement;
|
||||
onclick?: (e: Event, item: SearchItem) => void;
|
||||
prepend?: Snippet<[any]>;
|
||||
extraClasses?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
items = [],
|
||||
element = $bindable(),
|
||||
onclick,
|
||||
prepend,
|
||||
extraClasses,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
{id}
|
||||
class="menu absolute bg-menu-background border border-input-border rounded-xl shadow-md overflow-x-hidden overflow-y-scroll max-h-72 {extraClasses}"
|
||||
style="z-index: 1001"
|
||||
>
|
||||
{#if items.length == 0}
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
>
|
||||
{$_("no-results")}
|
||||
</li>
|
||||
{/if}
|
||||
{#each items as item}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
tabindex="0"
|
||||
onmousedown={(e) => onclick?.(e, item)}
|
||||
onkeydown={(e) => onclick?.(e, item)}
|
||||
>
|
||||
{#if prepend}{@render prepend({ item })}
|
||||
{:else if isURL(item.icon)}
|
||||
<img
|
||||
class="rounded-full w-8 mr-4 aspect-square"
|
||||
src={item.icon}
|
||||
alt="avatar"
|
||||
/>
|
||||
{:else}
|
||||
<i class="fa fa-{item.icon} basis-8 shrink-0"></i>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<p>{item.text}</p>
|
||||
{#if item.description}
|
||||
<p class="text-sm text-gray-500">
|
||||
{item.description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -37,7 +37,7 @@
|
||||
{/if}
|
||||
<select
|
||||
{name}
|
||||
class="block bg-input-background h-10 px-4 border-r-8 border-transparent outline outline-1 outline-input-border rounded-md focus:outline-input-border-focus transition-colors"
|
||||
class="block bg-input-background h-10 px-4 border-r-8 border-transparent outline-1 outline-input-border rounded-md focus:outline-input-border-focus transition-colors"
|
||||
class:text-gray-500={disabled}
|
||||
{disabled}
|
||||
bind:value
|
||||
|
||||
53
web/src/lib/components/base/skeleton_table.svelte
Normal file
53
web/src/lib/components/base/skeleton_table.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<div class="animate-pulse space-y-4 p-4">
|
||||
<!-- Table Header Skeleton -->
|
||||
<div class="flex items-center justify-between gap-4 px-2">
|
||||
<div class="h-5 w-20 bg-menu-item-background-focus rounded"></div>
|
||||
<div class="h-5 w-16 bg-menu-item-background-focus rounded"></div>
|
||||
<div class="h-5 w-24 bg-menu-item-background-focus rounded"></div>
|
||||
<div class="h-5 w-24 bg-menu-item-background-focus rounded"></div>
|
||||
<div class="h-5 w-16 bg-menu-item-background-focus rounded"></div>
|
||||
<div class="h-5 w-12 bg-menu-item-background-focus rounded"></div>
|
||||
<div class="h-5 w-8 bg-menu-item-background-focus rounded"></div>
|
||||
</div>
|
||||
|
||||
<!-- Repeatable Skeleton Rows -->
|
||||
<div class="space-y-2">
|
||||
{#each { length: 5 } as _, index}
|
||||
<!-- Row 1 -->
|
||||
<div class="flex items-center justify-between gap-4 px-2">
|
||||
<!-- Image placeholder -->
|
||||
<div
|
||||
class="h-10 w-10 bg-menu-item-background-focus rounded-lg"
|
||||
></div>
|
||||
<!-- Date -->
|
||||
<div
|
||||
class="h-4 w-20 bg-menu-item-background-focus rounded"
|
||||
></div>
|
||||
<!-- Distance -->
|
||||
<div
|
||||
class="h-4 w-16 bg-menu-item-background-focus rounded"
|
||||
></div>
|
||||
<!-- Elevation gain -->
|
||||
<div
|
||||
class="h-4 w-24 bg-menu-item-background-focus rounded"
|
||||
></div>
|
||||
<!-- Elevation loss -->
|
||||
<div
|
||||
class="h-4 w-24 bg-menu-item-background-focus rounded"
|
||||
></div>
|
||||
<!-- Duration -->
|
||||
<div
|
||||
class="h-4 w-12 bg-menu-item-background-focus rounded"
|
||||
></div>
|
||||
<!-- Author -->
|
||||
<div
|
||||
class="h-6 w-6 bg-menu-item-background-focus rounded-full"
|
||||
></div>
|
||||
<!-- Map icon -->
|
||||
<div
|
||||
class="h-5 w-5 bg-menu-item-background-focus rounded-md"
|
||||
></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,13 +44,16 @@
|
||||
});
|
||||
|
||||
export function set(value: number) {
|
||||
sliderContainer.noUiSlider.set(value)
|
||||
sliderContainer.noUiSlider.set(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="my-4" id="slider" bind:this={sliderContainer}></div>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
@reference "tailwindcss";
|
||||
@reference "../../../css/app.css";
|
||||
|
||||
:global(.noUi-target) {
|
||||
@apply border border-input-border shadow-none;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{#each toastStore.toasts as t, i}
|
||||
<div
|
||||
class="fixed px-4 py-3 shadow-xl rounded-xl border right-4 bg-white text-gray-500 flex items-center max-w-md"
|
||||
class="fixed px-4 py-3 shadow-xl rounded-xl border border-input-border right-4 bg-white text-gray-500 flex items-center max-w-md"
|
||||
style="z-index: 1001; bottom: {16 + i * 72}px"
|
||||
in:fade={{ duration: 250 }}
|
||||
out:fade={{ duration: 250 }}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Comment } from "$lib/models/comment";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { formatTimeSince } from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { fade } from "svelte/transition";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import Editor from "../base/editor.svelte";
|
||||
|
||||
interface Props {
|
||||
comment: Comment;
|
||||
mode?: "show" | "edit";
|
||||
ondelete?: (comment: Comment) => void
|
||||
onedit?: (data: {comment: Comment, text: string}) => void
|
||||
ondelete?: (comment: Comment) => void;
|
||||
onedit?: (data: { comment: Comment; text: string }) => void;
|
||||
}
|
||||
|
||||
let { comment, mode = "show", ondelete, onedit }: Props = $props();
|
||||
@@ -24,15 +25,15 @@
|
||||
});
|
||||
|
||||
let avatarSrc = $derived(
|
||||
comment.expand?.author.avatar
|
||||
? getFileURL(comment.expand.author, comment.expand.author.avatar)
|
||||
: `https://api.dicebear.com/7.x/initials/svg?seed=${comment.expand?.author.username ?? ""}&backgroundType=gradientLinear`,
|
||||
comment.expand?.author.icon
|
||||
? comment.expand?.author.icon
|
||||
: `https://api.dicebear.com/7.x/initials/svg?seed=${comment.expand?.author.preferred_username ?? ""}&backgroundType=gradientLinear`,
|
||||
);
|
||||
|
||||
let timeSince = $derived(formatTimeSince(new Date(comment.created ?? "")));
|
||||
|
||||
function deleteComment() {
|
||||
ondelete?.(comment)
|
||||
ondelete?.(comment);
|
||||
}
|
||||
|
||||
function toggleEdit() {
|
||||
@@ -48,38 +49,24 @@
|
||||
in:fade={{ duration: 150 }}
|
||||
out:fade={{ duration: 150 }}
|
||||
>
|
||||
{#if comment.expand?.author.private}
|
||||
<a
|
||||
href="/profile/{handleFromRecordWithIRI(comment)}"
|
||||
class="text-sm font-semibold shrink-0"
|
||||
>
|
||||
<img
|
||||
class="rounded-full w-10 aspect-square shrink-0"
|
||||
class="rounded-full w-10 aspect-square"
|
||||
src={avatarSrc}
|
||||
alt="avatar"
|
||||
/>
|
||||
{:else}
|
||||
<a
|
||||
href="/profile/{comment.expand?.author.id}"
|
||||
class="text-sm font-semibold shrink-0"
|
||||
>
|
||||
<img
|
||||
class="rounded-full w-10 aspect-square"
|
||||
src={avatarSrc}
|
||||
alt="avatar"
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
</a>
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<p class="">
|
||||
{#if comment.expand?.author.private}
|
||||
<span class="text-sm font-semibold"
|
||||
>{comment.expand?.author.username}</span
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
href="/profile/{comment.expand?.author.id}"
|
||||
class="text-sm font-semibold"
|
||||
>{comment.expand?.author.username}</a
|
||||
>
|
||||
{/if}
|
||||
<a
|
||||
href="/profile/{handleFromRecordWithIRI(comment)}"
|
||||
class="text-sm font-semibold"
|
||||
>{handleFromRecordWithIRI(comment)}</a
|
||||
>
|
||||
<span class="text-xs text-gray-500 ml-2"
|
||||
>{$_(`n-${timeSince.unit}-ago`, {
|
||||
values: { n: timeSince.value },
|
||||
@@ -103,15 +90,20 @@
|
||||
type="button"
|
||||
class="btn-icon text-xs"
|
||||
style="font-size: 0.75rem;"
|
||||
onclick={deleteComment}><i class="fa fa-trash"></i></button
|
||||
onclick={() => {
|
||||
if (editing) {
|
||||
editing = false;
|
||||
} else {
|
||||
deleteComment();
|
||||
}
|
||||
}}><i class="fa {editing ? 'fa-close' : 'fa-trash'}"></i></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if editing}
|
||||
<TextField extraClasses="mt-2" bind:value={editedComment}
|
||||
></TextField>
|
||||
<Editor extraClasses="mt-2" bind:value={editedComment}></Editor>
|
||||
{:else}
|
||||
<p class="whitespace-pre-wrap text-sm">{comment.text}</p>
|
||||
{@html comment.text}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<h5 class="font-semibold">Resources</h5>
|
||||
<ul class="mt-4 text-sm">
|
||||
<li>
|
||||
<a href="https://wanderer.to/getting-started/installation/"
|
||||
<a href="https://wanderer.to/run/installation/"
|
||||
>{$_("documentation")}</a
|
||||
>
|
||||
</li>
|
||||
@@ -36,7 +36,7 @@
|
||||
<a href="https://wanderer.to/#features">{$_("features")}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://wanderer.to/getting-started/changelog/"
|
||||
<a href="https://wanderer.to/run/changelog/"
|
||||
>{$_("changelog")}</a
|
||||
>
|
||||
</li>
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatHTMLAsText,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import ShareInfo from "../share_info.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
|
||||
interface Props {
|
||||
list: List;
|
||||
@@ -19,27 +22,9 @@
|
||||
|
||||
let { list, active = false }: Props = $props();
|
||||
|
||||
let cumulativeDistance = $derived(list.expand?.trails?.reduce(
|
||||
(s, b) => s + b.distance!,
|
||||
0,
|
||||
));
|
||||
|
||||
let cumulativeElevationGain = $derived(list.expand?.trails?.reduce(
|
||||
(s, b) => s + b.elevation_gain!,
|
||||
0,
|
||||
));
|
||||
|
||||
let cumulativeElevationLoss = $derived(list.expand?.trails?.reduce(
|
||||
(s, b) => s + b.elevation_loss!,
|
||||
0,
|
||||
));
|
||||
|
||||
let cumulativeDuration = $derived(list.expand?.trails?.reduce(
|
||||
(s, b) => s + b.duration!,
|
||||
0,
|
||||
));
|
||||
|
||||
let listIsShared = $derived((list.expand?.list_share_via_list?.length ?? 0) > 0);
|
||||
let listIsShared = $derived(
|
||||
(list.expand?.list_share_via_list?.length ?? 0) > 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -58,16 +43,21 @@
|
||||
|
||||
<div class="self-start min-w-0 basis-full transition-transform">
|
||||
<div class="flex items-center gap-3">
|
||||
<h5 class="text-xl font-semibold overflow-hidden overflow-ellipsis">
|
||||
<h5 class="text-xl font-semibold overflow-hidden overflow-ellipsis shrink-0 max-w-3/4 whitespace-nowrap">
|
||||
{list.name}
|
||||
</h5>
|
||||
{#if list.public}
|
||||
<span class="tooltip" data-title={$_("public")}>
|
||||
<i class="fa fa-globe"></i>
|
||||
</span>
|
||||
{/if}
|
||||
{#if listIsShared}
|
||||
<ShareInfo type="list" subject={list}></ShareInfo>
|
||||
<div class="basis-full"></div>
|
||||
{#if $currentUser}
|
||||
{#if list.public}
|
||||
<span class="tooltip" data-title={$_("public")}>
|
||||
<i class="fa fa-globe"></i>
|
||||
</span>
|
||||
{/if}
|
||||
{#if list.expand?.list_share_via_list?.length}
|
||||
<span class="tooltip" data-title={$_("shared")}>
|
||||
<i class="fa fa-share-nodes"></i>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if list.expand?.author}
|
||||
@@ -75,14 +65,11 @@
|
||||
{$_("by")}
|
||||
<img
|
||||
class="rounded-full w-5 aspect-square mx-1 inline"
|
||||
src={getFileURL(
|
||||
list.expand.author,
|
||||
list.expand.author.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.username}&backgroundType=gradientLinear`}
|
||||
src={list.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{list.expand?.author.username}
|
||||
{handleFromRecordWithIRI(list)}
|
||||
</p>
|
||||
{/if}
|
||||
<div
|
||||
@@ -90,27 +77,27 @@
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
cumulativeDistance,
|
||||
list.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||
cumulativeDuration,
|
||||
list.duration,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-arrow-trend-up mr-2"></i>{formatElevation(
|
||||
cumulativeElevationGain,
|
||||
list.elevation_gain,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-arrow-trend-down mr-2"></i>{formatElevation(
|
||||
cumulativeElevationLoss,
|
||||
list.elevation_loss,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
{list.expand?.trails?.length ?? 0}
|
||||
{list.trails?.length ?? 0}
|
||||
{$_("trail", {
|
||||
values: { n: list.expand?.trails?.length ?? 0 },
|
||||
})}
|
||||
@@ -120,7 +107,11 @@
|
||||
? ''
|
||||
: 'max-h-24 overflow-hidden text-ellipsis'}"
|
||||
>
|
||||
{!active ? list.description?.substring(0, 100) : list.description}
|
||||
{formatHTMLAsText(
|
||||
!active
|
||||
? list.description?.substring(0, 100)
|
||||
: list.description,
|
||||
)}
|
||||
{#if (list.description?.length ?? 0) > 100 && !active}
|
||||
...
|
||||
{/if}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
import ShareInfo from "../share_info.svelte";
|
||||
import TrailListItem from "../trail/trail_list_item.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
interface Props {
|
||||
list: List;
|
||||
onclick?: (data: { trail: Trail; index: number }) => void;
|
||||
@@ -43,15 +44,17 @@
|
||||
);
|
||||
|
||||
let allowEdit = $derived(
|
||||
list.author == $currentUser?.id ||
|
||||
list.author == $currentUser?.actor ||
|
||||
list.expand?.list_share_via_list?.some(
|
||||
(s) => s.permission == "edit",
|
||||
),
|
||||
);
|
||||
|
||||
let allowShare = $derived(
|
||||
list.author == $currentUser?.id &&
|
||||
!list.expand?.trails?.some((t) => t.author !== $currentUser?.id),
|
||||
list.author == $currentUser?.actor &&
|
||||
!list.expand?.trails?.some(
|
||||
(t) => !t.public && t.author !== $currentUser?.actor,
|
||||
),
|
||||
);
|
||||
|
||||
let dropdownItems = $derived([
|
||||
@@ -61,7 +64,7 @@
|
||||
...(allowEdit
|
||||
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
|
||||
: []),
|
||||
...(list.author == $currentUser?.id
|
||||
...(list.author == $currentUser?.actor
|
||||
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
|
||||
: []),
|
||||
]);
|
||||
@@ -88,7 +91,7 @@
|
||||
<div class="relative">
|
||||
{#if (list.public || listIsShared) && $currentUser}
|
||||
<div
|
||||
class="flex absolute top-4 right-6 {list.public && listIsShared
|
||||
class="flex absolute top-4 left-6 {list.public && listIsShared
|
||||
? 'w-16'
|
||||
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
|
||||
>
|
||||
@@ -139,20 +142,13 @@
|
||||
{$_("by")}
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mx-1 inline"
|
||||
src={getFileURL(
|
||||
list.expand.author,
|
||||
list.expand.author.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.username}&backgroundType=gradientLinear`}
|
||||
src={list.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${list.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{#if !list.expand.author.private}
|
||||
<a class="underline" href="/profile/{list.expand.author.id}"
|
||||
>{list.expand.author.username}</a
|
||||
>
|
||||
{:else}
|
||||
<span>{list.expand.author.username}</span>
|
||||
{/if}
|
||||
<a class="underline" href="/profile/{handleFromRecordWithIRI(list)}"
|
||||
>{handleFromRecordWithIRI(list)}</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
<hr />
|
||||
@@ -181,20 +177,22 @@
|
||||
>
|
||||
</div>
|
||||
<hr class="mb-4" />
|
||||
<p
|
||||
class="text-gray-500 whitespace-pre-wrap {fullDescription
|
||||
? ''
|
||||
: 'max-h-24 overflow-hidden text-ellipsis'}"
|
||||
>
|
||||
{!fullDescription
|
||||
? list.description?.substring(0, 100)
|
||||
: list.description}
|
||||
{#if (list.description?.length ?? 0) > 100 && !fullDescription}
|
||||
<button onclick={() => (fullDescription = true)}>
|
||||
... <span class="underline">{$_("read-more")}</span></button
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
{#if list.description}
|
||||
<p
|
||||
class="text-gray-500 whitespace-pre-wrap {fullDescription
|
||||
? ''
|
||||
: 'max-h-24 overflow-hidden text-ellipsis'} prose dark:prose-invert"
|
||||
>
|
||||
{@html !fullDescription
|
||||
? list.description?.substring(0, 100)
|
||||
: list.description}
|
||||
{#if (list.description?.length ?? 0) > 100 && !fullDescription}
|
||||
<button onclick={() => (fullDescription = true)}>
|
||||
... <span class="underline">{$_("read-more")}</span></button
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
<h5 class="text-xl font-semibold my-4">
|
||||
{list.trails?.length ?? 0}
|
||||
{$_("trail", { values: { n: list.trails?.length ?? 0 } })}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<Modal
|
||||
id="list-modal"
|
||||
title={$_("select-list")}
|
||||
size="max-w-sm"
|
||||
size="md:min-w-sm"
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet children({ openModal })}
|
||||
|
||||
@@ -12,30 +12,28 @@
|
||||
} from "$lib/stores/list_share_store";
|
||||
import {
|
||||
trail_share_create,
|
||||
trail_share_delete,
|
||||
trail_share_index,
|
||||
} from "$lib/stores/trail_share_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { _ } from "svelte-i18n";
|
||||
import ActorSearch from "../actor_search.svelte";
|
||||
import Button from "../base/button.svelte";
|
||||
import type { SelectItem } from "../base/select.svelte";
|
||||
import Select from "../base/select.svelte";
|
||||
import UserSearch from "../user_search.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
|
||||
interface Props {
|
||||
list: List;
|
||||
onsave?: () => void,
|
||||
onupdate?: (list: List) => void
|
||||
onsave?: () => void;
|
||||
onupdate?: (list: List) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
list,
|
||||
onsave,
|
||||
onupdate
|
||||
}: Props = $props();
|
||||
let { list, onsave, onupdate }: Props = $props();
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
let displayShareError = $state(false);
|
||||
|
||||
export function openModal() {
|
||||
openShareModal();
|
||||
}
|
||||
@@ -56,7 +54,7 @@
|
||||
|
||||
function copyURLToClipboard() {
|
||||
navigator.clipboard.writeText(
|
||||
window.location.href.split("?")[0] + "?list=" + list.id,
|
||||
`${window.location.origin}/lists/${handleFromRecordWithIRI(list)}/${list.id}`,
|
||||
);
|
||||
|
||||
copyButtonText = $_("link-copied");
|
||||
@@ -64,25 +62,36 @@
|
||||
}
|
||||
|
||||
function close() {
|
||||
onsave?.()
|
||||
onsave?.();
|
||||
modal.closeModal!();
|
||||
}
|
||||
|
||||
async function shareTrails(userId: string) {
|
||||
const existingTrailShares = await trail_share_index({ user: userId });
|
||||
async function shareTrails(actorIRI: string) {
|
||||
const existingTrailShares = await trail_share_index({
|
||||
actorIRI: actorIRI,
|
||||
});
|
||||
const trailIds = existingTrailShares.map((s) => s.trail);
|
||||
for (const trailId of list.trails ?? []) {
|
||||
if (!trailIds.includes(trailId)) {
|
||||
const share = new TrailShare(userId, trailId, "view");
|
||||
for (const trail of list.expand?.trails ?? []) {
|
||||
if (
|
||||
!trailIds.includes(trail.id!) &&
|
||||
!trail.public &&
|
||||
trail.author == $currentUser?.actor
|
||||
) {
|
||||
const share = new TrailShare(actorIRI, trail.id!, "view");
|
||||
await trail_share_create(share);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function shareList(item: SelectItem) {
|
||||
const share = new ListShare(item.value.id, list.id!, "view");
|
||||
if (!item.value.isLocal && !list.public) {
|
||||
displayShareError = true;
|
||||
return;
|
||||
}
|
||||
displayShareError = false;
|
||||
const share = new ListShare(item.value.iri, list.id!, "view");
|
||||
await list_share_create(share);
|
||||
await shareTrails(item.value.id);
|
||||
await shareTrails(item.value.iri);
|
||||
fetchShares();
|
||||
}
|
||||
|
||||
@@ -94,18 +103,6 @@
|
||||
await list_share_update(share);
|
||||
}
|
||||
|
||||
async function deleteTrailShares(userId: string) {
|
||||
const existingTrailShares = await trail_share_index({ user: userId });
|
||||
for (const trailId of list.trails ?? []) {
|
||||
const shareToDelete = existingTrailShares.find(
|
||||
(s) => s.trail == trailId,
|
||||
);
|
||||
if (shareToDelete) {
|
||||
await trail_share_delete(shareToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteShare(share: ListShare) {
|
||||
await list_share_delete(share);
|
||||
// await deleteTrailShares(share.user);
|
||||
@@ -116,6 +113,7 @@
|
||||
sharesLoading = true;
|
||||
const fetchedShares = await list_share_index(list.id!);
|
||||
list.expand = {
|
||||
...list.expand,
|
||||
trails: list.expand?.trails ?? [],
|
||||
list_share_via_list: fetchedShares.items,
|
||||
};
|
||||
@@ -127,18 +125,26 @@
|
||||
<Modal
|
||||
id="share-modal"
|
||||
title={$_("share-this-list")}
|
||||
size="max-w-sm overflow-visible"
|
||||
size="md:min-w-sm overflow-visible"
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet content()}
|
||||
<div>
|
||||
<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")}
|
||||
</p>
|
||||
<UserSearch
|
||||
includeSelf={false}
|
||||
onclick={(item) => shareList(item)}
|
||||
></UserSearch>
|
||||
{#if displayShareError}
|
||||
<p class="p-4 bg-red-100 rounded-xl mb-4 text-sm text-gray-500">
|
||||
<i class="fa fa-warning mr-2"></i>
|
||||
{$_("object-share-error", {
|
||||
values: { object: $_("trail", { values: { n: 1 } }) },
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
<ActorSearch includeSelf={false} onclick={(item) => shareList(item)}
|
||||
></ActorSearch>
|
||||
<h4 class="font-semibold mt-4">{$_("shared-with")}</h4>
|
||||
|
||||
{#if $shares.length == 0}
|
||||
@@ -151,22 +157,26 @@
|
||||
<div class="flex items-center gap-x-2 p-2">
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mr-2"
|
||||
src={getFileURL(
|
||||
share.expand.user,
|
||||
share.expand.user.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.user.username}&backgroundType=gradientLinear`}
|
||||
src={share.expand.actor.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<p>{share.expand.user.username}</p>
|
||||
<p>
|
||||
{`@${share.expand.actor.preferred_username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
||||
</p>
|
||||
<span
|
||||
class="basis-full text-sm text-center text-gray-500"
|
||||
>{$_("can")}</span
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class="shrink-0"
|
||||
class:tooltip={!share.expand.actor.isLocal}
|
||||
data-title={$_("remote-users-cannot-edit")}
|
||||
>
|
||||
<Select
|
||||
bind:value={share.permission}
|
||||
items={permissionSelectItems}
|
||||
disabled={!share.expand.actor.isLocal}
|
||||
onchange={(value) =>
|
||||
updateSharePermission(share, value)}
|
||||
></Select>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
case "/map":
|
||||
childPosition = 3;
|
||||
break;
|
||||
case "/lists":
|
||||
case "/lists/[[handle]]/[[id]]":
|
||||
childPosition = 4;
|
||||
break;
|
||||
default:
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (item.value == "profile") {
|
||||
goto(`/profile/${$currentUser?.id}`);
|
||||
goto(`/profile/@${$currentUser?.username?.toLowerCase()}`);
|
||||
} else if (item.value == "logout") {
|
||||
logout();
|
||||
window.location.href = "/";
|
||||
@@ -142,15 +142,21 @@
|
||||
<div class="basis-full"></div>
|
||||
<hr class="border-input-border" />
|
||||
<div class="flex gap-4 items-center justify-between m-4">
|
||||
<a class="shrink-0" href="/profile/{user.id}">
|
||||
<a
|
||||
class="shrink-0"
|
||||
href="/profile/@{user.username.toLowerCase()}"
|
||||
>
|
||||
<img
|
||||
class="rounded-full w-10 aspect-square"
|
||||
src={getFileURL(user, user.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${user.username}&backgroundType=gradientLinear`}
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${user.username.toLowerCase()}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
</a>
|
||||
<a href="/profile/{user.id}" style="width: calc(100% - 104px)">
|
||||
<a
|
||||
href="/profile/@{user.username.toLowerCase()}"
|
||||
style="width: calc(100% - 104px)"
|
||||
>
|
||||
<p class="text-sm overflow-hidden text-ellipsis">
|
||||
{user.username}
|
||||
</p>
|
||||
@@ -241,7 +247,7 @@
|
||||
<img
|
||||
class="rounded-full w-full h-full"
|
||||
src={getFileURL(user, user.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${user.username}&backgroundType=gradientLinear`}
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${user.username.toLowerCase()}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -3,33 +3,27 @@
|
||||
NotificationType,
|
||||
type Notification,
|
||||
} from "$lib/models/notification";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatTimeSince } from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
interface Props {
|
||||
notification: Notification;
|
||||
onclick?: (data: {notification: Notification, link: string | null}) => void
|
||||
onclick?: (data: {
|
||||
notification: Notification;
|
||||
link: string | null;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
let { notification, onclick }: Props = $props();
|
||||
|
||||
const avatarSrc = notification.expand?.author.avatar
|
||||
? getFileURL(
|
||||
notification.expand.author,
|
||||
notification.expand.author.avatar,
|
||||
)
|
||||
: `https://api.dicebear.com/7.x/initials/svg?seed=${notification.expand?.author.username ?? ""}&backgroundType=gradientLinear`;
|
||||
const avatarSrc = notification.expand?.author.icon
|
||||
? notification.expand.author.icon
|
||||
: `https://api.dicebear.com/7.x/initials/svg?seed=${notification.expand?.author.preferred_username ?? ""}&backgroundType=gradientLinear`;
|
||||
|
||||
const timeSince = formatTimeSince(new Date(notification.created ?? ""));
|
||||
|
||||
|
||||
function getTitle(n: Notification) {
|
||||
switch (n.type) {
|
||||
case NotificationType.listCreate:
|
||||
return $_("notification-list-create", {
|
||||
values: { user: n.expand.author.username },
|
||||
});
|
||||
case NotificationType.listShare:
|
||||
return $_("notification-list-share", {
|
||||
values: { user: n.expand.author.username },
|
||||
@@ -40,53 +34,84 @@
|
||||
return $_("notification-trail-comment", {
|
||||
values: {
|
||||
user: n.expand.author.username,
|
||||
trail: n.metadata?.trail,
|
||||
trail: n.metadata?.trail_name,
|
||||
},
|
||||
});
|
||||
case NotificationType.trailCreate:
|
||||
return $_("notification-trail-create", {
|
||||
values: { user: n.expand.author.username },
|
||||
});
|
||||
case NotificationType.trailShare:
|
||||
return $_("notification-trail-share", {
|
||||
values: { user: n.expand.author.username },
|
||||
});
|
||||
case NotificationType.summitLogCreate:
|
||||
return $_("notification-summit-log-create", {
|
||||
values: {
|
||||
user: n.expand.author.username,
|
||||
trail: n.metadata?.trail_name,
|
||||
},
|
||||
});
|
||||
case NotificationType.trailLike:
|
||||
return $_("notification-trail-like", {
|
||||
values: {
|
||||
user: n.expand.author.username,
|
||||
trail: n.metadata?.trail_name,
|
||||
},
|
||||
});
|
||||
case NotificationType.commentMention:
|
||||
return $_("notification-comment-mention", {
|
||||
values: {
|
||||
user: n.expand.author.username,
|
||||
trail: n.metadata?.trail_name,
|
||||
},
|
||||
});
|
||||
case NotificationType.trailMention:
|
||||
return $_("notification-trail-mention", {
|
||||
values: {
|
||||
user: n.expand.author.username,
|
||||
trail: n.metadata?.trail_name,
|
||||
},
|
||||
});
|
||||
case NotificationType.summitLogMention:
|
||||
return $_("notification-summit-log-mention", {
|
||||
values: {
|
||||
user: n.expand.author.username,
|
||||
trail: n.metadata?.trail_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDescription(n: Notification) {
|
||||
switch (n.type) {
|
||||
case NotificationType.listCreate:
|
||||
return n.metadata?.list ?? "";
|
||||
case NotificationType.listShare:
|
||||
return n.metadata?.list ?? "";
|
||||
case NotificationType.newFollower:
|
||||
return n.expand.author.username ?? "";
|
||||
case NotificationType.trailComment:
|
||||
return n.metadata?.comment ?? "";
|
||||
case NotificationType.trailCreate:
|
||||
return n.metadata?.trail ?? "";
|
||||
case NotificationType.trailShare:
|
||||
return n.metadata?.trail ?? "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getLink(n: Notification) {
|
||||
switch (n.type) {
|
||||
case NotificationType.listCreate:
|
||||
return `/lists?list=${n.metadata?.id}`;
|
||||
case NotificationType.listShare:
|
||||
return `/lists?list=${n.metadata?.id}`;
|
||||
return `/lists/${n.metadata?.author}/${n.metadata?.id}`;
|
||||
case NotificationType.newFollower:
|
||||
return n.expand.author.private === true
|
||||
? null
|
||||
: `/profile/${n.author}`;
|
||||
return `/profile/${n.metadata?.follower}`;
|
||||
case NotificationType.trailComment:
|
||||
return `/trail/view/${n.metadata?.id}?t=4`;
|
||||
case NotificationType.trailCreate:
|
||||
return `/trail/view/${n.metadata?.id}`;
|
||||
return `/trail/view/${n.metadata?.trail_author}/${n.metadata?.trail_id}?t=2`;
|
||||
case NotificationType.trailShare:
|
||||
return `/trail/view/${n.metadata?.id}`;
|
||||
return `/trail/view/${n.metadata?.author}/${n.metadata?.id}`;
|
||||
case NotificationType.summitLogCreate:
|
||||
return `/trail/view/${n.metadata?.trail_author}/${n.metadata?.trail_id}`;
|
||||
case NotificationType.trailLike:
|
||||
return `/trail/view/${n.metadata?.trail_author}/${n.metadata?.trail_id}`;
|
||||
case NotificationType.commentMention:
|
||||
return `/trail/view/${n.metadata?.trail_author}/${n.metadata?.trail_id}?t=2`;
|
||||
case NotificationType.trailMention:
|
||||
return `/trail/view/${n.metadata?.trail_author}/${n.metadata?.trail_id}`;
|
||||
case NotificationType.summitLogMention:
|
||||
return `/trail/view/${n.metadata?.trail_author}/${n.metadata?.trail_id}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +132,10 @@
|
||||
<div>
|
||||
<p
|
||||
class="text-sm {notification.seen
|
||||
? 'font-medium'
|
||||
? 'font-normal'
|
||||
: 'font-semibold'} mr-3"
|
||||
>
|
||||
{title}:
|
||||
{title}
|
||||
</p>
|
||||
<p class="text-sm line-clamp-1">{description}</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
|
||||
@@ -30,11 +30,9 @@
|
||||
notifications.reduce((value, n) => (value += n.seen ? 0 : 1), 0),
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
if (!notifications.length && page.data.notifications?.items?.length) {
|
||||
notifications = page.data.notifications.items;
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
notifications = page.data.notifications.items
|
||||
})
|
||||
|
||||
async function toggleMenu(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
@@ -66,7 +64,7 @@
|
||||
scrollTop + clientHeight >= scrollHeight * 0.8 &&
|
||||
pagination.page !== pagination.totalPages &&
|
||||
!loadingNextPage
|
||||
) {
|
||||
) {
|
||||
await loadNextPage();
|
||||
}
|
||||
}
|
||||
@@ -79,7 +77,7 @@
|
||||
}
|
||||
pagination.page += 1;
|
||||
const result = await notifications_index(
|
||||
{ recipient: $currentUser.id },
|
||||
{ recipient: $currentUser.actor },
|
||||
pagination.page,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
<script lang="ts">
|
||||
import type { Activity } from "$lib/models/activity";
|
||||
import type { UserAnonymous } from "$lib/models/user";
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { TimelineItem } from "$lib/models/timeline";
|
||||
import { getFileURL, isVideoURL } from "$lib/util/file_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatHTMLAsText,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
interface Props {
|
||||
activity: Activity;
|
||||
user: UserAnonymous;
|
||||
activity: TimelineItem;
|
||||
actor: Actor;
|
||||
}
|
||||
|
||||
let { activity, user }: Props = $props();
|
||||
let { activity, actor }: Props = $props();
|
||||
|
||||
let fullDescription: boolean = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="activity-card p-6 space-y-6 rounded-xl border border-input-border">
|
||||
<div class="activity-card p-6 ounded-xl border border-input-border">
|
||||
<div class="flex gap-x-4 items-start">
|
||||
<img
|
||||
class="rounded-full w-10 aspect-square overflow-hidden"
|
||||
src={getFileURL(user, user.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${user.username}&backgroundType=gradientLinear`}
|
||||
src={actor.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${actor.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div>
|
||||
{#if !user.private}
|
||||
<a class="underline" href="/profile/{user.id}"
|
||||
>{user.username}</a
|
||||
>
|
||||
{:else}
|
||||
<span>{user.username}</span>
|
||||
{/if}
|
||||
<span class="font-semibold">{actor.preferred_username}</span>
|
||||
{activity.type === "trail"
|
||||
? $_("planned-a-trail")
|
||||
: $_("completed-a-trail")}
|
||||
@@ -74,9 +69,9 @@
|
||||
<div
|
||||
class="grid gap-[1px] {activity.photos.length > 1
|
||||
? 'grid-cols-[8fr_5fr]'
|
||||
: 'grid-cols-1'}"
|
||||
: 'grid-cols-1'} mt-6"
|
||||
>
|
||||
{#each activity.photos.slice(0,3) as photo, i}
|
||||
{#each activity.photos.slice(0, 3) as photo, i}
|
||||
{#if isVideoURL(photo)}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video
|
||||
@@ -109,10 +104,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if activity.description.length}
|
||||
<p class="text-sm whitespace-pre-wrap">
|
||||
{!fullDescription
|
||||
? activity.description?.substring(0, 100)
|
||||
: activity.description}
|
||||
<p class="text-sm whitespace-pre-wrap mt-6">
|
||||
{formatHTMLAsText(
|
||||
!fullDescription
|
||||
? activity.description?.substring(0, 100)
|
||||
: activity.description,
|
||||
)}
|
||||
{#if (activity.description?.length ?? 0) > 100 && !fullDescription}
|
||||
<button
|
||||
onclick={(e) => {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<Modal
|
||||
id="password-modal"
|
||||
size="max-w-xl"
|
||||
size="md:max-w-xl"
|
||||
title={$_("share-profile")}
|
||||
bind:this={modal}
|
||||
>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<Modal
|
||||
id="email-modal"
|
||||
size="max-w-xl"
|
||||
size="md:min-w-md"
|
||||
title={$_("change-email")}
|
||||
bind:this={modal}
|
||||
>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
<Modal
|
||||
id="komoot-settings-modal"
|
||||
size="max-w-lg"
|
||||
size="md:max-w-lg"
|
||||
title={"komoot " + $_("settings")}
|
||||
bind:this={modal}
|
||||
>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<Modal
|
||||
id="strava-settings-modal"
|
||||
size="max-w-lg"
|
||||
size="md:min-w-lg"
|
||||
title={"strava " + $_("settings")}
|
||||
bind:this={modal}
|
||||
>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
<Modal
|
||||
id="password-modal"
|
||||
size="max-w-sm"
|
||||
size="md:min-w-sm"
|
||||
title={$_("change-password")}
|
||||
bind:this={modal}
|
||||
>
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
<button
|
||||
class="underline"
|
||||
onclick={() =>
|
||||
goto(`/trail/view/${u.duplicate!.id}`)}
|
||||
goto(`/trail/view/@${u.duplicate?.domain}/${u.duplicate!.id}`)}
|
||||
>{u.duplicate.name}</button
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
<Modal
|
||||
id="url-import-modal"
|
||||
size="max-w-xl"
|
||||
size="md:min-w-sm"
|
||||
title={$_("from-url")}
|
||||
bind:this={modal}
|
||||
>
|
||||
@@ -88,7 +88,7 @@
|
||||
{/snippet}
|
||||
{#snippet footer()}
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="btn-secondary" onclick={() => modal.closeModal()}
|
||||
<button type="button" class="btn-secondary" onclick={() => modal.closeModal()}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<Button primary type="submit" form="url-import-form" {loading}>{$_("import")}</Button>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { List } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import type { UserAnonymous } from "$lib/models/user";
|
||||
import { currentUser, users_show } from "$lib/stores/user_store";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { fly } from "svelte/transition";
|
||||
@@ -22,29 +24,7 @@
|
||||
|
||||
let showInfo: boolean = $state(false);
|
||||
let loading: boolean = $state(false);
|
||||
let infoLoaded: boolean = $state(false);
|
||||
let subjectIsOwned: boolean = subject.author == $currentUser?.id;
|
||||
let author: UserAnonymous | undefined = $state();
|
||||
|
||||
async function fetchInfo() {
|
||||
if (!infoLoaded) {
|
||||
loading = true;
|
||||
loading = false;
|
||||
if (subjectIsOwned) {
|
||||
for (const share of shareData ?? []) {
|
||||
share.expand = {
|
||||
user: await users_show(share.user),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
author = await users_show(subject.author!);
|
||||
}
|
||||
|
||||
infoLoaded = true;
|
||||
}
|
||||
|
||||
showInfo = true;
|
||||
}
|
||||
let subjectIsOwned: boolean = subject.author == $currentUser?.actor;
|
||||
</script>
|
||||
|
||||
<div class="relative trail-share-info">
|
||||
@@ -53,18 +33,18 @@
|
||||
class:text-xl={large}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onmouseenter={fetchInfo}
|
||||
onmouseenter={() => (showInfo = true)}
|
||||
onmouseleave={() => (showInfo = false)}
|
||||
></i>
|
||||
{#if showInfo}
|
||||
<ul
|
||||
class="menu absolute z-10 top-8 bg-menu-background border border-input-border rounded-xl shadow-md overflow-hidden p-3 space-y-3 text-content -translate-x-3/4"
|
||||
class="menu absolute z-10 top-8 bg-menu-background border border-input-border rounded-xl shadow-md overflow-hidden p-3 space-y-3 text-content"
|
||||
in:fly={{ y: -10, duration: 150 }}
|
||||
out:fly={{ y: -10, duration: 150 }}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="spinner spinner-dark"></div>
|
||||
{:else if infoLoaded && shareData}
|
||||
{:else if shareData}
|
||||
{#if subjectIsOwned}
|
||||
{#each shareData as share}
|
||||
{#if share.expand}
|
||||
@@ -72,15 +52,12 @@
|
||||
<div class="flex items-center mr-8">
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mr-2"
|
||||
src={getFileURL(
|
||||
share.expand.user,
|
||||
share.expand.user.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.user.username}&backgroundType=gradientLinear`}
|
||||
src={share.expand.actor.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<p class="font-semibold text-base">
|
||||
{share.expand.user.username}
|
||||
{`@${share.expand.actor.username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
||||
</p>
|
||||
<span class="mx-2 text-sm text-gray-500"
|
||||
>{$_("can")}</span
|
||||
@@ -96,7 +73,7 @@
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if author}
|
||||
{:else if subject.expand?.author}
|
||||
<li>
|
||||
<p class="text-xs text-gray-500 mb-2">
|
||||
{$_("shared-by")}
|
||||
@@ -104,12 +81,12 @@
|
||||
<div class="flex items-center mr-8">
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mr-2"
|
||||
src={getFileURL(author, author.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${author.username}&backgroundType=gradientLinear`}
|
||||
src={subject.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${subject.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<p class="font-semibold">
|
||||
{author.username}
|
||||
{handleFromRecordWithIRI(subject)}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
|
||||
@@ -6,12 +6,18 @@
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { getFileURL, isVideoURL, readAsDataURLAsync } from "$lib/util/file_util";
|
||||
import {
|
||||
getFileURL,
|
||||
isVideoURL,
|
||||
readAsDataURLAsync,
|
||||
} from "$lib/util/file_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatHTMLAsText,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
|
||||
let thumbnail: string = $state(
|
||||
$theme === "light" ? emptyStateTrailLight : emptyStateTrailDark,
|
||||
@@ -91,6 +97,18 @@
|
||||
<Dropdown items={dropdownItems} {onchange}></Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
{#if log.expand?.author}
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
{$_("by")}
|
||||
<img
|
||||
class="rounded-full w-5 aspect-square mx-1 inline"
|
||||
src={log.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${log.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{handleFromRecordWithIRI(log)}
|
||||
</p>
|
||||
{/if}
|
||||
{#if log.distance || log.elevation_gain || log.elevation_loss || log.duration}
|
||||
<div
|
||||
class="flex mt-1 gap-x-4 text-sm text-gray-500 flex-wrap mb-2"
|
||||
@@ -102,7 +120,7 @@
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||
log.duration ? log.duration / 60 : undefined,
|
||||
log.duration ? log.duration : undefined,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
@@ -115,7 +133,7 @@
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="whitespace-pre-wrap">{log.text}</span>
|
||||
<span class="whitespace-pre-wrap">{formatHTMLAsText(log.text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import PhotoPicker from "../trail/photo_picker.svelte";
|
||||
import TrailPicker from "../trail/trail_picker.svelte";
|
||||
import type { SummitLog } from "$lib/models/summit_log";
|
||||
import Editor from "../base/editor.svelte";
|
||||
interface Props {
|
||||
children?: Snippet<[any]>;
|
||||
onsave?: (summitLog: SummitLog) => void;
|
||||
@@ -108,6 +109,7 @@
|
||||
<Modal
|
||||
id="summit-log-modal"
|
||||
title={$data.id ? $_("edit-entry") : $_("add-entry")}
|
||||
size="md:min-w-2xl"
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet children({ openModal })}
|
||||
@@ -134,7 +136,7 @@
|
||||
showThumbnailControls={false}
|
||||
></PhotoPicker>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex gap-4 items-end">
|
||||
{#if $data.expand}
|
||||
<TrailPicker
|
||||
bind:trailFile={$data._gpx}
|
||||
@@ -144,12 +146,13 @@
|
||||
></TrailPicker>
|
||||
{/if}
|
||||
<div class="basis-full">
|
||||
<Textarea
|
||||
name="text"
|
||||
extraClasses="h-28"
|
||||
<Editor
|
||||
bind:value={$data.text}
|
||||
extraClasses="min-h-28"
|
||||
label={$_("text")}
|
||||
error={$errors.text}
|
||||
></Textarea>
|
||||
searchListPosition="fixed"
|
||||
></Editor>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,23 +10,32 @@
|
||||
import { _ } from "svelte-i18n";
|
||||
import MapWithElevationMaplibre from "../trail/map_with_elevation_maplibre.svelte";
|
||||
import { fetchGPX } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
|
||||
interface Props {
|
||||
summitLogs?: SummitLog[];
|
||||
handle: string;
|
||||
showCategory?: boolean;
|
||||
showTrail?: boolean;
|
||||
showAuthor?: boolean;
|
||||
showRoute?: boolean;
|
||||
showPhotos?: boolean;
|
||||
showMenu?: boolean;
|
||||
ondelete?: (summitLog: SummitLog) => void;
|
||||
onedit?: (summitLog: SummitLog) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
summitLogs = [],
|
||||
handle,
|
||||
showCategory = false,
|
||||
showTrail = false,
|
||||
showAuthor = false,
|
||||
showRoute = false,
|
||||
showPhotos = false,
|
||||
showMenu = false,
|
||||
ondelete,
|
||||
onedit,
|
||||
}: Props = $props();
|
||||
|
||||
let mapModal: Modal;
|
||||
@@ -64,7 +73,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="w-full">
|
||||
<table class="w-full table-auto">
|
||||
<thead class="text-left text-gray-500">
|
||||
<tr class="text-sm">
|
||||
{#if showPhotos && summitLogs.some((l) => l.photos.length)}
|
||||
@@ -98,12 +107,16 @@
|
||||
{$_("map")}
|
||||
</th>
|
||||
{/if}
|
||||
{#if showMenu}
|
||||
<th> </th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each summitLogs as log, i}
|
||||
<SummitLogTableRow
|
||||
{log}
|
||||
{handle}
|
||||
onopen={(log) => openMap(log)}
|
||||
ontext={(log) => openText(log)}
|
||||
{showCategory}
|
||||
@@ -113,6 +126,9 @@
|
||||
summitLogs.some((l) => l.photos.length)}
|
||||
showDescription={summitLogs.some((l) => l.text?.length)}
|
||||
showRoute={showRoute && summitLogs.some((l) => l.gpx)}
|
||||
showMenu={showMenu && log.author == $currentUser?.actor}
|
||||
{ondelete}
|
||||
{onedit}
|
||||
></SummitLogTableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
@@ -122,7 +138,7 @@
|
||||
{/if}
|
||||
<Modal
|
||||
id="summit-log-table-map-modal"
|
||||
size="max-w-4xl"
|
||||
size="md:min-w-2xl lg:min-w-4xl"
|
||||
title=""
|
||||
bind:this={mapModal}
|
||||
>
|
||||
@@ -137,17 +153,17 @@
|
||||
</Modal>
|
||||
<Modal
|
||||
id="summit-log-table-text-modal"
|
||||
size="max-w-xl"
|
||||
size="md:min-w-xl"
|
||||
title={$_("description")}
|
||||
bind:this={textModal}
|
||||
>
|
||||
{#snippet content()}
|
||||
<p class="whitespace-pre-wrap">{currentText}</p>
|
||||
<div class="prose dark:prose-invert">{@html currentText}</div>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
th {
|
||||
padding: 0rem 0.5rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,38 +5,59 @@
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatHTMLAsText,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import PhotoGallery from "../photo_gallery.svelte";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
|
||||
interface Props {
|
||||
log: SummitLog;
|
||||
handle: string;
|
||||
showCategory?: boolean;
|
||||
showTrail?: boolean;
|
||||
showRoute?: boolean;
|
||||
showAuthor?: boolean;
|
||||
showDescription?: boolean;
|
||||
showPhotos?: boolean;
|
||||
showMenu?: boolean;
|
||||
ontext?: (summitLog: SummitLog) => void;
|
||||
onopen?: (summitLog: SummitLog) => void;
|
||||
ondelete?: (summitLog: SummitLog) => void;
|
||||
onedit?: (summitLog: SummitLog) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
log,
|
||||
handle,
|
||||
showCategory = false,
|
||||
showTrail = false,
|
||||
showRoute = false,
|
||||
showAuthor = false,
|
||||
showDescription = false,
|
||||
showPhotos = false,
|
||||
showMenu = false,
|
||||
onopen,
|
||||
ontext,
|
||||
ondelete,
|
||||
onedit,
|
||||
}: Props = $props();
|
||||
|
||||
let gallery: PhotoGallery;
|
||||
|
||||
let imgSrc: string[] = $state([]);
|
||||
|
||||
let dropdownItems: DropdownItem[] = [
|
||||
{
|
||||
text: $_("edit"),
|
||||
value: "edit",
|
||||
},
|
||||
{
|
||||
text: $_("delete"),
|
||||
value: "delete",
|
||||
},
|
||||
];
|
||||
$effect(() => {
|
||||
if (log.photos?.length) {
|
||||
imgSrc = log.photos
|
||||
@@ -65,6 +86,14 @@
|
||||
showDescription,
|
||||
].reduce((b, v) => (v ? b + 1 : b), 7);
|
||||
}
|
||||
|
||||
function handleDropdownClick(item: DropdownItem): void {
|
||||
if (item.value == "edit") {
|
||||
onedit?.(log);
|
||||
} else if (item.value == "delete") {
|
||||
ondelete?.(log);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr class="text-sm">
|
||||
@@ -126,14 +155,11 @@
|
||||
{formatElevation(log.elevation_loss)}
|
||||
</td>
|
||||
<td>
|
||||
{formatTimeHHMM(log.duration ? log.duration / 60 : undefined)}
|
||||
{formatTimeHHMM(log.duration ? log.duration : undefined)}
|
||||
</td>
|
||||
{#if showCategory}
|
||||
<td>
|
||||
{$_(
|
||||
log.expand?.trails_via_summit_logs?.at(0)?.expand?.category
|
||||
?.name ?? "-",
|
||||
)}
|
||||
{$_(log.expand?.trail?.expand?.category?.name ?? "-")}
|
||||
</td>
|
||||
{/if}
|
||||
{#if showTrail}
|
||||
@@ -141,8 +167,7 @@
|
||||
<a
|
||||
aria-label="Go to trail"
|
||||
class="btn-icon aspect-square"
|
||||
href="/trail/view/{log.expand?.trails_via_summit_logs?.at(0)
|
||||
?.id ?? ''}"
|
||||
href="/trail/view/{handle}/{log.expand?.trail?.id ?? ''}"
|
||||
><i class="fa fa-arrow-up-right-from-square px-[3px]"></i></a
|
||||
>
|
||||
</td>
|
||||
@@ -154,7 +179,7 @@
|
||||
><p
|
||||
class="rounded-full bg-menu-item-background-hover hover:bg-menu-item-background-focus text-ellipsis max-w-28 whitespace-nowrap overflow-hidden px-3 py-1"
|
||||
>
|
||||
{log.text}
|
||||
{formatHTMLAsText(log.text)}
|
||||
</p></button
|
||||
>
|
||||
{/if}
|
||||
@@ -164,43 +189,54 @@
|
||||
<td>
|
||||
<p
|
||||
class="tooltip flex justify-center"
|
||||
data-title={log.expand.author.username}
|
||||
data-title="@{log.expand.author.preferred_username}{log.expand.author
|
||||
.isLocal
|
||||
? ''
|
||||
: '@' + log.expand.author.domain}"
|
||||
>
|
||||
{#if !log.expand.author.private}
|
||||
<a href="/profile/{log.expand.author.id}">
|
||||
<img
|
||||
class="rounded-full w-7 aspect-square"
|
||||
src={getFileURL(
|
||||
log.expand?.author,
|
||||
log.expand?.author.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${log.expand?.author.username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href="/profile/@{log.expand.author.preferred_username?.toLowerCase()}{log
|
||||
.expand.author.isLocal
|
||||
? ''
|
||||
: '@' + log.expand.author.domain}"
|
||||
>
|
||||
<img
|
||||
class="rounded-full w-7 aspect-square"
|
||||
src={getFileURL(
|
||||
log.expand?.author,
|
||||
log.expand?.author.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${log.expand?.author.username}&backgroundType=gradientLinear`}
|
||||
src={log.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${log.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{/if}
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
{/if}
|
||||
{#if showRoute && log.gpx}
|
||||
{#if showRoute}
|
||||
<td>
|
||||
<button
|
||||
aria-label="Open route"
|
||||
onclick={openRoute}
|
||||
class="btn-icon"
|
||||
>
|
||||
<i class="fa fa-map-location-dot px-[3px] text-xl"></i></button
|
||||
>
|
||||
{#if log.gpx}
|
||||
<button
|
||||
aria-label="Open route"
|
||||
onclick={openRoute}
|
||||
class="btn-icon"
|
||||
>
|
||||
<i class="fa fa-map-location-dot px-[3px] text-xl"
|
||||
></i></button
|
||||
>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
{#if showMenu}
|
||||
<td>
|
||||
<Dropdown onchange={handleDropdownClick} items={dropdownItems}>
|
||||
{#snippet children({ toggleMenu: openDropdown })}
|
||||
<button
|
||||
aria-label="Open dropdown"
|
||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-6 w-6"
|
||||
onclick={openDropdown}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
{/snippet}
|
||||
</Dropdown>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
|
||||
112
web/src/lib/components/trail/like_button.svelte
Normal file
112
web/src/lib/components/trail/like_button.svelte
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { TrailLike } from "$lib/models/trail_like";
|
||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||
import {
|
||||
trail_like_create,
|
||||
trail_like_delete,
|
||||
} from "$lib/stores/trail_like_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import confetti from "canvas-confetti";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
}
|
||||
|
||||
let likeButton: HTMLButtonElement;
|
||||
|
||||
let { trail }: Props = $props();
|
||||
|
||||
let likeCount = $state(trail.like_count);
|
||||
|
||||
let loading = $state(false);
|
||||
|
||||
async function like(e: Event) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!$currentUser || !trail.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
if (trailLike) {
|
||||
await trail_like_delete(trailLike);
|
||||
likeCount -= 1;
|
||||
trailLike = undefined;
|
||||
} else {
|
||||
trailLike = await trail_like_create(
|
||||
new TrailLike($currentUser.actor, trail.id),
|
||||
);
|
||||
likeCount += 1;
|
||||
|
||||
const rect = likeButton.getBoundingClientRect();
|
||||
const x = (rect.left + rect.width / 2) / window.innerWidth;
|
||||
const y = (rect.top + rect.height / 2) / window.innerHeight;
|
||||
|
||||
confetti({
|
||||
particleCount: 60,
|
||||
startVelocity: 20,
|
||||
spread: 40,
|
||||
origin: { x, y },
|
||||
colors: ["#fb2c36"],
|
||||
scalar: 1.2,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
show_toast({
|
||||
icon: "close",
|
||||
text: $_("error-liking-trail"),
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
let trailLike = $state(
|
||||
trail.expand?.trail_like_via_trail?.find(
|
||||
(l) => l.actor === $currentUser?.actor,
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="like-button rounded-full w-12 py-2 bg-background {$currentUser ? 'hover:bg-secondary-hover' : ''} transition-colors relative"
|
||||
aria-label="like"
|
||||
type="button"
|
||||
bind:this={likeButton}
|
||||
disabled={!$currentUser}
|
||||
onclick={like}
|
||||
data-title={$_("likes")}
|
||||
>
|
||||
<i
|
||||
class="{trailLike ? 'fa' : 'fa-regular'} fa-heart text-3xl"
|
||||
class:text-red-500={trailLike !== undefined}
|
||||
class:loading
|
||||
></i>
|
||||
<p class="font-semibold">{likeCount}</p>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.like-button > i.loading {
|
||||
animation: heartbeat 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
40% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,12 +13,10 @@
|
||||
createPopupFromTrail,
|
||||
FontawesomeMarker,
|
||||
} from "$lib/util/maplibre_util";
|
||||
import { polylineToGeoJSON } from "$lib/util/polyline_util";
|
||||
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
|
||||
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
|
||||
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
|
||||
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
|
||||
import { T } from "@threlte/core";
|
||||
import type { Feature, FeatureCollection, GeoJSON } from "geojson";
|
||||
import * as M from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
@@ -466,6 +464,9 @@
|
||||
},
|
||||
layout: {
|
||||
"text-field": "{point_count_abbreviated}",
|
||||
"text-font": [
|
||||
"Noto Sans Regular",
|
||||
],
|
||||
"text-size": 12,
|
||||
},
|
||||
});
|
||||
@@ -868,12 +869,12 @@
|
||||
}),
|
||||
),
|
||||
{
|
||||
text: "Open Street Maps",
|
||||
value: "/styles/osm.json",
|
||||
text: "OpenFreeMap",
|
||||
value: "/styles/ofm.json",
|
||||
thumbnail: "https://tile.openstreetmap.org/1/0/0.png",
|
||||
},
|
||||
{
|
||||
text: "Open Topo Maps",
|
||||
text: "OpenTopoMap",
|
||||
value: "/styles/otm.json",
|
||||
thumbnail: "https://tile.opentopomap.org/1/0/0.png",
|
||||
},
|
||||
@@ -1135,6 +1136,17 @@
|
||||
});
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
const isInputField =
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable;
|
||||
|
||||
if (isInputField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key == "m") {
|
||||
if (trails.length === 1) {
|
||||
removeCaretLayer();
|
||||
@@ -1144,6 +1156,17 @@
|
||||
}
|
||||
|
||||
function handleKeyup(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
const isInputField =
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable;
|
||||
|
||||
if (isInputField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key == "m") {
|
||||
if (trails.length === 1) {
|
||||
addTrailLayer(trails[0], trails[0].id!, 0, data[0]);
|
||||
@@ -1160,7 +1183,10 @@
|
||||
<svelte:window on:keydown={handleKeydown} on:keyup={handleKeyup} />
|
||||
<div id="map" bind:this={mapContainer}></div>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
@reference "tailwindcss";
|
||||
@reference "../../../css/app.css";
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
|
||||
let thumbnail = $derived(
|
||||
trail.photos.length
|
||||
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
|
||||
? getFileURL(
|
||||
trail,
|
||||
trail.photos.at(trail.thumbnail ?? 0) ?? trail.photos[0],
|
||||
"600x0"
|
||||
)
|
||||
: $theme === "light"
|
||||
? emptyStateTrailLight
|
||||
: emptyStateTrailDark,
|
||||
@@ -80,6 +84,20 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $currentUser && trail.like_count > 0}
|
||||
<div
|
||||
class="flex absolute items-center justify-center top-4 left-4 bg-background w-8 h-8 rounded-full"
|
||||
>
|
||||
<span class="tooltip" data-title={$_("likes")}>
|
||||
<i class="fa fa-heart"></i>
|
||||
</span>
|
||||
<div
|
||||
class="absolute pointer-events-none left-5 -top-1 text-xs rounded-full bg-menu-background px-1 text-center"
|
||||
>
|
||||
{trail.like_count}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if (trail.public || trailIsShared) && $currentUser}
|
||||
<div
|
||||
class="flex absolute top-4 right-4 {trail.public && trailIsShared
|
||||
@@ -120,14 +138,13 @@
|
||||
{$_("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`}
|
||||
src={trail.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{trail.expand.author.username}
|
||||
{trail.expand.author.preferred_username}{trail.expand.author.isLocal
|
||||
? ""
|
||||
: "@" + trail.expand.author.domain}
|
||||
</p>
|
||||
{/if}
|
||||
{#if trail.expand?.tags?.length}
|
||||
|
||||
@@ -23,10 +23,11 @@
|
||||
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
handle: string;
|
||||
mode: "overview" | "map" | "list";
|
||||
}
|
||||
|
||||
let { trail, mode }: Props = $props();
|
||||
let { trail, handle, mode }: Props = $props();
|
||||
|
||||
let confirmModal: ConfirmModal;
|
||||
let listSelectModal: ListSelectModal;
|
||||
@@ -35,8 +36,10 @@
|
||||
|
||||
let lists: List[] = $state([]);
|
||||
|
||||
const isOwned: boolean = trail.author == $currentUser?.actor;
|
||||
|
||||
const allowEdit =
|
||||
trail.author == $currentUser?.id ||
|
||||
isOwned ||
|
||||
trail.expand?.trail_share_via_trail?.some(
|
||||
(s) => s.permission == "edit",
|
||||
);
|
||||
@@ -61,16 +64,16 @@
|
||||
]
|
||||
: []),
|
||||
{ text: $_("print"), value: "print", icon: "print" },
|
||||
...(trail.author != $currentUser?.id
|
||||
? []
|
||||
: [{ text: $_("add-to-list"), value: "list", icon: "bookmark" }]),
|
||||
...(trail.author != $currentUser?.id
|
||||
? []
|
||||
: [{ text: $_("share"), value: "share", icon: "share" }]),
|
||||
...(isOwned
|
||||
? [{ text: $_("add-to-list"), value: "list", icon: "bookmark" }]
|
||||
: []),
|
||||
...(isOwned
|
||||
? [{ text: $_("share"), value: "share", icon: "share" }]
|
||||
: []),
|
||||
...(allowEdit
|
||||
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
|
||||
: []),
|
||||
...(trail.author == $currentUser?.id
|
||||
...(isOwned
|
||||
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
|
||||
: []),
|
||||
];
|
||||
@@ -79,13 +82,13 @@
|
||||
if (item.value == "show") {
|
||||
goto(
|
||||
mode == "overview"
|
||||
? `/map/trail/${trail.id!}`
|
||||
: `/trail/view/${trail.id!}`,
|
||||
? `/map/trail/${handle}/${trail.id!}`
|
||||
: `/trail/view/${handle}/${trail.id!}`,
|
||||
);
|
||||
} else if (item.value == "list") {
|
||||
lists = (
|
||||
await lists_index(
|
||||
{ q: "", author: $currentUser?.id ?? "" },
|
||||
{ q: "", author: $currentUser?.actor ?? "" },
|
||||
1,
|
||||
-1,
|
||||
)
|
||||
@@ -99,7 +102,7 @@
|
||||
)
|
||||
?.focus();
|
||||
} else if (item.value == "print") {
|
||||
goto(`/map/trail/${trail.id}/print`);
|
||||
goto(`/map/trail/${handle}/${trail.id}/print`);
|
||||
} else if (item.value == "share") {
|
||||
trailShareModal.openModal();
|
||||
} else if (item.value == "download") {
|
||||
@@ -155,7 +158,7 @@
|
||||
}
|
||||
if (exportSettings.summitLog) {
|
||||
let summitLogString = "";
|
||||
for (const summitLog of trail.expand?.summit_logs ?? []) {
|
||||
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) {
|
||||
summitLogString += `${summitLog.date},${summitLog.text}\n`;
|
||||
}
|
||||
zip.file(
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal id="export-modal" {title} size="max-w-sm" bind:this={modal}>
|
||||
<Modal id="export-modal" {title} size="md:min-w-sm" bind:this={modal}>
|
||||
{#snippet content()}
|
||||
<div>
|
||||
<div class="mb-3">
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import type { TrailFilter } from "$lib/models/trail";
|
||||
import { searchLocations } from "$lib/stores/search_store";
|
||||
import { tags_index } from "$lib/stores/tag_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { formatDistance, formatElevation } from "$lib/util/format_util";
|
||||
import { getIconForLocation } from "$lib/util/icon_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { slide } from "svelte/transition";
|
||||
import ActorSearch from "../actor_search.svelte";
|
||||
import Combobox, { type ComboboxItem } from "../base/combobox.svelte";
|
||||
import Datepicker from "../base/datepicker.svelte";
|
||||
import DoubleSlider from "../base/double_slider.svelte";
|
||||
@@ -16,8 +18,6 @@
|
||||
import Search, { type SearchItem } from "../base/search.svelte";
|
||||
import type { SelectItem } from "../base/select.svelte";
|
||||
import Slider from "../base/slider.svelte";
|
||||
import UserSearch from "../user_search.svelte";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
|
||||
interface Props {
|
||||
categories: Category[];
|
||||
@@ -39,12 +39,12 @@
|
||||
|
||||
let categorySelectItems = $derived(
|
||||
categories.map((c) => ({
|
||||
value: c.id,
|
||||
value: c.name,
|
||||
text: c.name,
|
||||
})),
|
||||
);
|
||||
|
||||
const radioGroupItems: RadioItem[] = [
|
||||
const radioGroupCompletenessItems: RadioItem[] = [
|
||||
{ text: $_("completed"), value: "completed" },
|
||||
{ text: $_("not-completed"), value: "not_completed" },
|
||||
{ text: $_("no-preference"), value: "no_preference" },
|
||||
@@ -82,13 +82,13 @@
|
||||
update();
|
||||
}
|
||||
|
||||
function setPublicFilter(e: Event) {
|
||||
filter.public = (e.target as HTMLInputElement).checked;
|
||||
function setSharedFilter(e: Event) {
|
||||
filter.shared = (e.target as HTMLInputElement).checked;
|
||||
update();
|
||||
}
|
||||
|
||||
function setSharedFilter(e: Event) {
|
||||
filter.shared = (e.target as HTMLInputElement).checked;
|
||||
function setLikedFilter(e: Event) {
|
||||
filter.liked = (e.target as HTMLInputElement).checked;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -111,6 +111,16 @@
|
||||
update();
|
||||
}
|
||||
|
||||
function setPrivateFilter(e: Event) {
|
||||
filter.private = (e.target as HTMLInputElement).checked;
|
||||
update();
|
||||
}
|
||||
|
||||
function setPublicFilter(e: Event) {
|
||||
filter.public = (e.target as HTMLInputElement).checked;
|
||||
update();
|
||||
}
|
||||
|
||||
async function searchCities(q: string) {
|
||||
if (q.length == 0) {
|
||||
filter.near.lat = undefined;
|
||||
@@ -150,6 +160,20 @@
|
||||
filter.tags = tags.map((t) => t.text);
|
||||
update();
|
||||
}
|
||||
|
||||
function getVisibiltyStatus(): number {
|
||||
const isPublic = filter.public !== undefined && filter.public === true;
|
||||
const isPrivate =
|
||||
filter.private !== undefined && filter.private === true;
|
||||
|
||||
if (isPublic === true && isPrivate === true) {
|
||||
return 2;
|
||||
} else if (isPublic === true) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="trail-filter p-8 border border-input-border rounded-xl">
|
||||
@@ -198,7 +222,7 @@
|
||||
<hr class="my-4 border-separator" />
|
||||
|
||||
{#if $currentUser}
|
||||
<UserSearch
|
||||
<ActorSearch
|
||||
onclick={(item) => setAuthorFilter(item)}
|
||||
onclear={() => {
|
||||
filter.author = "";
|
||||
@@ -206,7 +230,24 @@
|
||||
}}
|
||||
clearAfterSelect={false}
|
||||
label={$_("author")}
|
||||
></UserSearch>
|
||||
></ActorSearch>
|
||||
<hr class="my-4 border-separator" />
|
||||
|
||||
<p class="text-sm font-medium">{$_("visibilty-status")}</p>
|
||||
|
||||
<div class="flex items-center mt-2 mb-4">
|
||||
<input
|
||||
id="private-checkbox"
|
||||
type="checkbox"
|
||||
checked={filter.private}
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
onchange={setPrivateFilter}
|
||||
/>
|
||||
|
||||
<label for="private-checkbox" class="ms-2 text-sm"
|
||||
>{$_("private")}</label
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center my-4">
|
||||
<input
|
||||
id="public-checkbox"
|
||||
@@ -231,6 +272,7 @@
|
||||
>{$_("shared")}</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<hr class="my-4 border-separator" />
|
||||
{/if}
|
||||
<MultiSelect
|
||||
@@ -334,13 +376,28 @@
|
||||
onchange={update}
|
||||
></Datepicker>
|
||||
</div>
|
||||
|
||||
<hr class="my-4 border-separator" />
|
||||
<p class="text-sm font-medium pb-4">{$_("like-status")}</p>
|
||||
<input
|
||||
id="liked-checkbox"
|
||||
type="checkbox"
|
||||
checked={filter.liked}
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
onchange={setLikedFilter}
|
||||
/>
|
||||
<label for="liked-checkbox" class="ms-2 text-sm"
|
||||
>{$_("liked")}</label
|
||||
>
|
||||
<hr class="my-4 border-separator" />
|
||||
<p class="text-sm font-medium pb-4">{$_("completion-status")}</p>
|
||||
<RadioGroup
|
||||
name="completed"
|
||||
items={radioGroupItems}
|
||||
selected={2}
|
||||
items={radioGroupCompletenessItems}
|
||||
selected={filter.completed === undefined
|
||||
? 2
|
||||
: filter.completed === true
|
||||
? 0
|
||||
: 1}
|
||||
onchange={(item) => setCompletedFilter(item)}
|
||||
></RadioGroup>
|
||||
</div>
|
||||
|
||||
@@ -42,9 +42,25 @@
|
||||
import SummitLogTable from "../summit_log/summit_log_table.svelte";
|
||||
import MapWithElevationMaplibre from "./map_with_elevation_maplibre.svelte";
|
||||
import TrailTimeline from "./trail_timeline.svelte";
|
||||
import {
|
||||
summit_logs_create,
|
||||
summit_logs_delete,
|
||||
summit_logs_index,
|
||||
summit_logs_update,
|
||||
summitLog,
|
||||
summitLogs,
|
||||
} from "$lib/stores/summit_log_store";
|
||||
import { SummitLog } from "$lib/models/summit_log";
|
||||
import SummitLogModal from "../summit_log/summit_log_modal.svelte";
|
||||
import SkeletonTable from "../base/skeleton_table.svelte";
|
||||
import ConfirmModal from "../confirm_modal.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import LikeButton from "./like_button.svelte";
|
||||
import Editor from "../base/editor.svelte";
|
||||
|
||||
interface Props {
|
||||
initTrail: Trail;
|
||||
handle: string;
|
||||
mode?: "overview" | "map" | "list";
|
||||
markers?: M.Marker[];
|
||||
activeTab?: number;
|
||||
@@ -52,11 +68,15 @@
|
||||
|
||||
let {
|
||||
initTrail,
|
||||
handle,
|
||||
mode = "map",
|
||||
markers = [],
|
||||
activeTab = 0,
|
||||
}: Props = $props();
|
||||
|
||||
let summitLogModal: SummitLogModal;
|
||||
let confirmModal: ConfirmModal;
|
||||
|
||||
let trail = $state(initTrail);
|
||||
|
||||
const tabs = [
|
||||
@@ -72,15 +92,17 @@
|
||||
|
||||
let newComment: Comment = $state({
|
||||
text: "",
|
||||
rating: 0,
|
||||
author: "",
|
||||
trail: trail.id ?? "",
|
||||
trail: handle + "/" + (trail.id ?? ""),
|
||||
});
|
||||
|
||||
let commentsLoading: boolean = $state(activeTab == 2);
|
||||
let commentCreateLoading: boolean = $state(false);
|
||||
let commentDeleteLoading: boolean = false;
|
||||
|
||||
let summitLogsLoading: boolean = $state(activeTab == 0);
|
||||
let summitLogCreateLoading: boolean = $state(false);
|
||||
|
||||
let fullDescription: boolean = $state(false);
|
||||
|
||||
onMount(async () => {});
|
||||
@@ -100,13 +122,23 @@
|
||||
}
|
||||
|
||||
async function toggleMapFullScreen() {
|
||||
goto(`/map/trail/${trail.id!}`);
|
||||
goto(`/map/trail/${handle}/${trail.id!}`);
|
||||
}
|
||||
|
||||
async function fetchComments() {
|
||||
commentsLoading = true;
|
||||
await comments_index(trail);
|
||||
commentsLoading = false;
|
||||
const trailId = trail.iri ? trail.iri : trail.id!;
|
||||
try {
|
||||
await comments_index(trailId, handle);
|
||||
} catch (e) {
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error loading comments.",
|
||||
});
|
||||
} finally {
|
||||
commentsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createComment() {
|
||||
@@ -114,15 +146,12 @@
|
||||
return;
|
||||
}
|
||||
commentCreateLoading = true;
|
||||
newComment.author = $currentUser.id;
|
||||
newComment.author = $currentUser.actor;
|
||||
newComment.trail = trail.id;
|
||||
|
||||
try {
|
||||
const c = await comments_create(newComment);
|
||||
newComment.text = "";
|
||||
c.expand = {
|
||||
author: { ...$currentUser!, private: false },
|
||||
};
|
||||
|
||||
const newCommentList = [c, ...$comments];
|
||||
comments.set(newCommentList);
|
||||
@@ -167,6 +196,70 @@
|
||||
fetchComments();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (activeTab == 0) {
|
||||
fetchSummitLog();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchSummitLog() {
|
||||
summitLogsLoading = true;
|
||||
const trailId = trail.iri ? trail.iri : trail.id;
|
||||
try {
|
||||
await summit_logs_index({ trail: trailId, category: [] }, handle);
|
||||
} catch (e) {
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error loading summit logs.",
|
||||
});
|
||||
} finally {
|
||||
summitLogsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function beforeSummitLogModalOpen(currentSummitLog?: SummitLog) {
|
||||
currentSummitLog ??= new SummitLog(
|
||||
new Date().toISOString().split("T")[0],
|
||||
);
|
||||
|
||||
summitLog.set(currentSummitLog);
|
||||
summitLogModal.openModal();
|
||||
}
|
||||
|
||||
async function saveSummitLog(log: SummitLog) {
|
||||
summitLogCreateLoading = true;
|
||||
if (log.id) {
|
||||
let oldLogIndex = $summitLogs.findIndex((l) => l.id === log.id);
|
||||
if (oldLogIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const updatedLog = await summit_logs_update(
|
||||
$summitLogs[oldLogIndex],
|
||||
log,
|
||||
);
|
||||
$summitLogs[oldLogIndex] = updatedLog;
|
||||
} else {
|
||||
log.trail = trail.id;
|
||||
const newLog = await summit_logs_create(log);
|
||||
summitLogs.set([...$summitLogs, newLog]);
|
||||
}
|
||||
summitLogCreateLoading = false;
|
||||
}
|
||||
|
||||
function beforeConfirmModalOpen(currentSummitLog: SummitLog) {
|
||||
summitLog.set(currentSummitLog);
|
||||
confirmModal.openModal();
|
||||
}
|
||||
|
||||
async function deleteSummitLog() {
|
||||
await summit_logs_delete($summitLog);
|
||||
const newSummitLogList = $summitLogs.filter(
|
||||
(l) => l.id !== $summitLog.id,
|
||||
);
|
||||
summitLogs.set(newSummitLogList);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -283,22 +376,13 @@
|
||||
{$_("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`}
|
||||
src={trail.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{#if !trail.expand.author.private}
|
||||
<a
|
||||
class="underline"
|
||||
href="/profile/{trail.expand.author.id}"
|
||||
>{trail.expand.author.username}</a
|
||||
>
|
||||
{:else}
|
||||
<span>{trail.expand.author.username}</span>
|
||||
{/if}
|
||||
<a class="underline" href="/profile/{handle}"
|
||||
>{handleFromRecordWithIRI(trail)}</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex flex-wrap gap-x-8 gap-y-2 mt-2 mr-8">
|
||||
@@ -314,8 +398,11 @@
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{#if ($currentUser && $currentUser.id == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public}
|
||||
<TrailDropdown {trail} {mode}></TrailDropdown>
|
||||
{#if ($currentUser && $currentUser.actor == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public}
|
||||
<div class="flex flex-col items-center gap-y-2">
|
||||
<LikeButton {trail}></LikeButton>
|
||||
<TrailDropdown {trail} {mode} {handle}></TrailDropdown>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -386,8 +473,10 @@
|
||||
{$_("description")}
|
||||
</h4>
|
||||
{#if trail.description?.length}
|
||||
<article class="text-justify whitespace-pre-line text-sm">
|
||||
{!fullDescription
|
||||
<article
|
||||
class="text-justify whitespace-pre-line text-sm prose dark:prose-invert"
|
||||
>
|
||||
{@html !fullDescription
|
||||
? trail.description?.substring(0, 300)
|
||||
: trail.description}
|
||||
{#if (trail.description?.length ?? 0) > 300 && !fullDescription}
|
||||
@@ -422,17 +511,39 @@
|
||||
onmouseleave={closeMarkerPopup}
|
||||
></TrailTimeline>
|
||||
|
||||
<div class="mb-6 mt-12">
|
||||
<div class="mb-6 mt-12 flex justify-between flex-wrap gap-y-4">
|
||||
<Tabs {tabs} bind:activeTab></Tabs>
|
||||
{#if activeTab == 0 && mode != "list"}
|
||||
<Button
|
||||
secondary
|
||||
type="button"
|
||||
loading={summitLogCreateLoading}
|
||||
disabled={summitLogCreateLoading}
|
||||
onclick={() => beforeSummitLogModalOpen()}
|
||||
><i class="fa fa-plus mr-2"></i>{$_(
|
||||
"add-entry",
|
||||
)}</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if activeTab == 0}
|
||||
<div class="overflow-x-auto">
|
||||
<SummitLogTable
|
||||
summitLogs={trail.expand?.summit_logs}
|
||||
showAuthor
|
||||
showRoute
|
||||
showPhotos
|
||||
></SummitLogTable>
|
||||
<div
|
||||
class="overflow-x-auto overflow-y-clip pb-3 scroll-x-only min-h-[175px]"
|
||||
>
|
||||
{#if summitLogsLoading}
|
||||
<SkeletonTable></SkeletonTable>
|
||||
{:else}
|
||||
<SummitLogTable
|
||||
{handle}
|
||||
summitLogs={$summitLogs}
|
||||
showAuthor
|
||||
showRoute
|
||||
showPhotos
|
||||
showMenu
|
||||
onedit={(log) => beforeSummitLogModalOpen(log)}
|
||||
ondelete={(log) => beforeConfirmModalOpen(log)}
|
||||
></SummitLogTable>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if activeTab == 1}
|
||||
@@ -483,15 +594,15 @@
|
||||
$currentUser,
|
||||
$currentUser.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username?.toLowerCase()}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div class="basis-full">
|
||||
<Textarea
|
||||
<Editor
|
||||
bind:value={newComment.text}
|
||||
rows={2}
|
||||
extraClasses="min-h-24 max-w-full"
|
||||
placeholder="Add comment..."
|
||||
></Textarea>
|
||||
></Editor>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-3">
|
||||
@@ -521,7 +632,7 @@
|
||||
<CommentCard
|
||||
{comment}
|
||||
mode={comment.author ==
|
||||
$currentUser?.id
|
||||
$currentUser?.actor
|
||||
? "edit"
|
||||
: "show"}
|
||||
ondelete={deleteComment}
|
||||
@@ -547,7 +658,7 @@
|
||||
elevationProfileContainer={"epc-container"}
|
||||
showStyleSwitcher={false}
|
||||
showFullscreen={true}
|
||||
mapOptions={{ attributionControl: false }}
|
||||
mapOptions={{ attributionControl: {compact: true} }}
|
||||
onfullscreen={toggleMapFullScreen}
|
||||
bind:markers
|
||||
></MapWithElevationMaplibre>
|
||||
@@ -557,6 +668,16 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<SummitLogModal bind:this={summitLogModal} onsave={(log) => saveSummitLog(log)}
|
||||
></SummitLogModal>
|
||||
|
||||
<ConfirmModal
|
||||
id="confirm-summit-log-delete-modal"
|
||||
text={$_("delete-summit-log-confirm")}
|
||||
bind:this={confirmModal}
|
||||
onconfirm={deleteSummitLog}
|
||||
></ConfirmModal>
|
||||
|
||||
<style>
|
||||
.trail-info-panel img {
|
||||
object-fit: cover;
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
{ text: $_("difficulty"), value: "difficulty" },
|
||||
{ text: $_("elevation-gain"), value: "elevation_gain" },
|
||||
{ text: $_("elevation-loss"), value: "elevation_loss" },
|
||||
{ text: $_("likes"), value: "like_count" },
|
||||
{ text: $_("creation-date"), value: "created" },
|
||||
{ text: $_("date"), value: "date" },
|
||||
];
|
||||
@@ -98,7 +99,7 @@
|
||||
onupdate?.(filter);
|
||||
}
|
||||
|
||||
function handleSortUpdate(sort: any) {
|
||||
function handleSortUpdate(sort: any) {
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +191,9 @@
|
||||
<a
|
||||
class="max-w-full flex-1"
|
||||
class:basis-full={selectedDisplayOption === "list"}
|
||||
href="/trail/view/{trail.id}"
|
||||
href="/trail/view/@{trail.author}{trail.domain
|
||||
? `@${trail.domain}`
|
||||
: ''}/{trail.id}"
|
||||
>
|
||||
{#if selectedDisplayOption === "cards"}
|
||||
<TrailCard fullWidth={fullWidthCards} {trail}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatHTMLAsText,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import ShareInfo from "../share_info.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
@@ -22,7 +24,11 @@
|
||||
|
||||
let thumbnail = $derived(
|
||||
trail.photos.length
|
||||
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
|
||||
? getFileURL(
|
||||
trail,
|
||||
trail.photos.at(trail.thumbnail ?? 0) ?? trail.photos[0],
|
||||
"600x0",
|
||||
)
|
||||
: $theme === "light"
|
||||
? emptyStateTrailLight
|
||||
: emptyStateTrailDark,
|
||||
@@ -50,18 +56,32 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 basis-full">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-semibold text-lg">
|
||||
{trail.name}
|
||||
</h4>
|
||||
{#if trail.public && $currentUser}
|
||||
<span class="tooltip ml-3" data-title={$_("public")}>
|
||||
<i class="fa fa-globe"></i>
|
||||
</span>
|
||||
{/if}
|
||||
{#if trail.expand?.trail_share_via_trail?.length}
|
||||
<ShareInfo type="trail" subject={trail}></ShareInfo>
|
||||
{/if}
|
||||
<div class="flex items-center shrink-0 gap-3">
|
||||
{#if trail.public && $currentUser}
|
||||
<span class="tooltip" data-title={$_("public")}>
|
||||
<i class="fa fa-globe"></i>
|
||||
</span>
|
||||
{/if}
|
||||
{#if trail.expand?.trail_share_via_trail?.length}
|
||||
<ShareInfo type="trail" subject={trail}></ShareInfo>
|
||||
{/if}
|
||||
{#if $currentUser && trail.like_count > 0}
|
||||
<div class="flex items-start gap-x-[2px]">
|
||||
<span class="tooltip" data-title={$_("likes")}>
|
||||
<i class="fa fa-heart"></i>
|
||||
</span>
|
||||
<div
|
||||
class="pointer-events-none left-4 -top-1 text-xs rounded-full bg-content text-content-inverse px-1 text-center"
|
||||
>
|
||||
{trail.like_count}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if trail.date}
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
@@ -78,14 +98,11 @@
|
||||
{$_("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`}
|
||||
src={trail.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
{trail.expand.author.username}
|
||||
{handleFromRecordWithIRI(trail)}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex flex-wrap gap-x-8">
|
||||
@@ -123,7 +140,7 @@
|
||||
<p
|
||||
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
|
||||
>
|
||||
{trail.description}
|
||||
{formatHTMLAsText(trail.description ?? "")}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
trail_share_index,
|
||||
trail_share_update,
|
||||
} from "$lib/stores/trail_share_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import ActorSearch from "../actor_search.svelte";
|
||||
import Button from "../base/button.svelte";
|
||||
import type { SelectItem } from "../base/select.svelte";
|
||||
import Select from "../base/select.svelte";
|
||||
import UserSearch from "../user_search.svelte";
|
||||
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
@@ -25,6 +24,8 @@
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
let displayShareError = $state(false);
|
||||
|
||||
export function openModal() {
|
||||
openShareModalLocal();
|
||||
}
|
||||
@@ -55,7 +56,12 @@
|
||||
}
|
||||
|
||||
async function shareTrail(item: SelectItem) {
|
||||
const share = new TrailShare(item.value.id, trail.id!, "view");
|
||||
if (!item.value.isLocal && !trail.public) {
|
||||
displayShareError = true;
|
||||
return;
|
||||
}
|
||||
displayShareError = false;
|
||||
const share = new TrailShare(item.value.iri, trail.id!, "view");
|
||||
await trail_share_create(share);
|
||||
fetchShares();
|
||||
}
|
||||
@@ -83,13 +89,23 @@
|
||||
<Modal
|
||||
id="share-modal"
|
||||
title={$_("share-this-trail")}
|
||||
size="max-w-sm overflow-visible"
|
||||
size="md:min-w-sm overflow-visible"
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet content()}
|
||||
<div>
|
||||
<UserSearch includeSelf={false} onclick={(item) => shareTrail(item)}
|
||||
></UserSearch>
|
||||
{#if displayShareError}
|
||||
<p class="p-4 bg-red-100 rounded-xl mb-4 text-sm text-gray-500">
|
||||
<i class="fa fa-warning mr-2"></i>
|
||||
{$_("object-share-error", {
|
||||
values: { object: $_("trail", { values: { n: 1 } }) },
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
<ActorSearch
|
||||
includeSelf={false}
|
||||
onclick={(item) => shareTrail(item)}
|
||||
></ActorSearch>
|
||||
<h4 class="font-semibold mt-4">{$_("shared-with")}</h4>
|
||||
|
||||
{#if $shares.length == 0}
|
||||
@@ -102,22 +118,26 @@
|
||||
<div class="flex items-center gap-x-2 p-2">
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square mr-2"
|
||||
src={getFileURL(
|
||||
share.expand.user,
|
||||
share.expand.user.avatar,
|
||||
) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.user.username}&backgroundType=gradientLinear`}
|
||||
src={share.expand.actor.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<p>{share.expand.user.username}</p>
|
||||
<p>
|
||||
{`@${share.expand.actor.username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
||||
</p>
|
||||
<span
|
||||
class="basis-full text-sm text-center text-gray-500"
|
||||
class="basis-full text-sm text-gray-500 text-end"
|
||||
>{$_("can")}</span
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class="shrink-0"
|
||||
class:tooltip={!share.expand.actor.isLocal}
|
||||
data-title={$_("remote-users-cannot-edit")}
|
||||
>
|
||||
<Select
|
||||
value={share.permission}
|
||||
items={permissionSelectItems}
|
||||
disabled={!share.expand.actor.isLocal}
|
||||
onchange={(value) =>
|
||||
updateSharePermission(share, value)}
|
||||
></Select>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
tableHeader: SelectItem[];
|
||||
trails?: Trail[] | null;
|
||||
filter?: TrailFilter | null;
|
||||
onsort?: (value: any) => void
|
||||
onsort?: (value: any) => void;
|
||||
}
|
||||
|
||||
let { tableHeader, trails = null, filter = null, onsort }: Props = $props();
|
||||
@@ -71,7 +71,12 @@
|
||||
{#each trails as trail}
|
||||
<tr
|
||||
class="border-t border-input-border cursor-pointer hover:bg-secondary-hover transition-colors"
|
||||
onclick={() => goto(`/trail/view/${trail.id}`)}
|
||||
onclick={() =>
|
||||
goto(
|
||||
`/trail/view/@${trail.author}${
|
||||
trail.domain ? `@${trail.domain}` : ""
|
||||
}/${trail.id}`,
|
||||
)}
|
||||
>
|
||||
<td
|
||||
class="flex justify-between items-center text-sm relative"
|
||||
@@ -83,13 +88,10 @@
|
||||
{#if trail.expand && trail.expand.author}
|
||||
<div class="author-icon">
|
||||
<img
|
||||
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} ${trail.expand.author.username}`}
|
||||
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} @${trail.expand.author.preferred_username}${trail.expand.author.isLocal ? "" : "@" + trail.expand.author.domain}`}
|
||||
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`}
|
||||
src={trail.expand.author.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
@@ -122,6 +124,9 @@
|
||||
<td class="p-4 text-sm">
|
||||
{formatElevation(trail.elevation_gain)}
|
||||
</td>
|
||||
<td class="p-4 text-sm">
|
||||
{trail.like_count}
|
||||
</td>
|
||||
<td class="p-4 text-sm">
|
||||
{#if trail.created}
|
||||
{new Date(trail.created).toLocaleDateString(
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
|
||||
<Modal
|
||||
id="waypoint-modal"
|
||||
size="md:min-w-2xl"
|
||||
title={$data.id ? $_("edit-waypoint") : $_("add-waypoint")}
|
||||
bind:this={modal}
|
||||
>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Löschen",
|
||||
"delete-account": "Konto löschen",
|
||||
"delete-list-confirm": "Möchtest Du diese Liste wirklich löschen? Die Routen in der Liste sind danach weiterhin verfügbar.",
|
||||
"delete-summit-log-confirm": "Wollen sie diesen Gipfebuch Eintrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"delete-trail-confirm": "Möchtest Du diese Route wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"describe-your-trail": "Beschreibe deine Route",
|
||||
"description": "Beschreibung",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Fehler beim Login",
|
||||
"error-during-password-reset": "Email zum Zurücksetzen des Passworts konnte nicht versandt werden",
|
||||
"error-exporting-trail": "Fehler beim Exportieren der Route",
|
||||
"error-liking-trail": "Fehler diesem Trail ein \"Gefällt mir\" zu geben",
|
||||
"error-logging-in-to-komoot": "Fehler bei der Anmeldung bei komoot",
|
||||
"error-posting-comment": "Fehler beim Posten des Kommentars",
|
||||
"error-printing-map": "Fehler beim Drucken der Karte",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixe Geschwindigkeit",
|
||||
"focus-map-on": "Karte fokussieren auf",
|
||||
"follow": "Folgen",
|
||||
"follow-request-pending": "Anfrage ausstehend",
|
||||
"followers": "Follower",
|
||||
"following": "Folgt",
|
||||
"forgot-your-password": "Passwort vergessen?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "Deutsch",
|
||||
"get-position-from-exif": "Koordinaten aus EXIF Daten",
|
||||
"grid": "Gitter",
|
||||
"heading": "Heading",
|
||||
"height": "Höhe",
|
||||
"help": "Hilfe",
|
||||
"hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "Importieren",
|
||||
"import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...",
|
||||
"include-description": "Beschreibung übernehmen",
|
||||
"include-waypoints": "Wegpunkte einbeziehen",
|
||||
"integration-description-komoot": "Synchronisiert Deine komoot Touren regelmäßig mit wanderer.",
|
||||
"integration-description-strava": "Synchronisiert Deine strava Routen und Aktivitäten regelmäßig mit wanderer.",
|
||||
"integration-disabled": "Integration deaktiviert",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Sprache",
|
||||
"latitude": "Breitengrad",
|
||||
"license": "Lizenz",
|
||||
"like-status": "\"Gefällt mir\" Status",
|
||||
"liked": "Gefällt mir",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link kopiert!",
|
||||
"list": "{n, plural, =1 {Liste} other {Listen}}",
|
||||
"list-not-shared": "Mit niemandem geteilt",
|
||||
@@ -213,7 +221,7 @@
|
||||
"no-comments-so-far": "Bisher keine Kommentare",
|
||||
"no-data": "Keine Daten",
|
||||
"no-description-for-now": "Noch keine Beschreibung",
|
||||
"no-gps-data-in-image": "Keine GPS-Daten im Bild",
|
||||
"no-gps-data-in-image": "",
|
||||
"no-grid": "Kein Gitter",
|
||||
"no-notifications": "Keine Benachrichtigungen",
|
||||
"no-photos-here": "Hier sind noch keine Fotos",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Keine gültige Email-Adresse",
|
||||
"not-a-valid-url": "Keine gültige URL",
|
||||
"not-completed": "Nicht abgeschlossen",
|
||||
"notification-comment-mention": "{user} hat Dich in einem Kommentar erwähnt",
|
||||
"notification-list-create": "{user} hat eine neue Liste erstellt",
|
||||
"notification-list-share": "{user} hat eine Liste mit Dir geteilt",
|
||||
"notification-new-follower": "Du hast einen neuen Follower",
|
||||
"notification-summit-log-create": "{user} hat einen Gipfelbuch Eintrag zu deiner Route erstellt",
|
||||
"notification-summit-log-mention": "{user} hat Dich in einem Gipfelbuch Eintrag erwähnt",
|
||||
"notification-trail-comment": "{user} hat einen Kommentar zu Deiner Route \"{trail}\" hinterlassen",
|
||||
"notification-trail-create": "{user} hat eine neue Route erstellt",
|
||||
"notification-trail-like": "{user} hat deinem Trail {trail} ein \"Gefällt mir\" gegeben",
|
||||
"notification-trail-mention": "{user} hat dich in seiner Route erwähnt",
|
||||
"notification-trail-share": "{user} hat eine Route mit Dir geteilt",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"object-share-error": "Eine {object} muss öffentlich sein, um über Instanzen hinweg geteilt zu werden.",
|
||||
"off": "Aus",
|
||||
"only-me": "Nur ich",
|
||||
"open-in-new-tab": "In neuem Tab öffnen",
|
||||
"or": "oder",
|
||||
"orientation": "Ausrichtung",
|
||||
"paper-size": "Papierformat",
|
||||
"paragraph": "Paragraph",
|
||||
"password": "Passwort",
|
||||
"password-confirm": "Passwort bestätigen",
|
||||
"password-reset-sent": "Email zum Zurücksetzen des Passworts versandt",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Radius",
|
||||
"read-more": "Mehr",
|
||||
"register": "Registrieren",
|
||||
"remote-users-cannot-edit": "Nutzer anderer Instanzen können nicht bearbeiten",
|
||||
"removed-trail-from": "Route entfernt aus",
|
||||
"required": "Pflichtfeld",
|
||||
"reset-password": "Passwort zurücksetzen",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Route suchen",
|
||||
"select-list": "Liste auswählen",
|
||||
"settings": "Einstellungen",
|
||||
"settings-notification-comment-mention": "Jemand hat dich in einem Kommentar erwähnt",
|
||||
"settings-notification-list-create": "Ein Benutzer, dem Du folgst, hat eine Liste erstellt",
|
||||
"settings-notification-list-share": "Jemand hat eine Liste mit Dir geteilt",
|
||||
"settings-notification-new-follower": "Du hast einen neuen Follower",
|
||||
"settings-notification-summit-log-create": "Jemand hat einen Gipfelbuch Eintrag zu deiner Route erstellt",
|
||||
"settings-notification-summit-log-mention": "Jemand hat dich in einem Gipfelbuch Eintrag erwähnt",
|
||||
"settings-notification-trail-comment": "Jemand hat einen Kommentar zu Deiner Route hinterlassen",
|
||||
"settings-notification-trail-create": "Ein Benutzer, dem Du folgst, hat eine Route erstellt",
|
||||
"settings-notification-trail-like": "Jemand hat deiner Route ein \"Gefällt mir\" gegeben",
|
||||
"settings-notification-trail-mention": "Jemand hat dich in einer Route erwähnt",
|
||||
"settings-notification-trail-share": "Jemand hat eine Route mit Dir geteilt",
|
||||
"settings-privacy-account-private": "Nur Du kannst dein Profil sehen. Du wirst nicht in den Suchergebnissen angezeigt. Andere Benutzer können Dir nicht folgen oder Routen mit Dir teilen. Du kannst weiterhin Routen oder Listen veröffentlichen.",
|
||||
"settings-privacy-account-public": "Jeder kann Dein Profil sehen. Du erscheinst in den Suchergebnissen. Andere Benutzer können Dir folgen und Routen mit Dir teilen.",
|
||||
@@ -292,7 +314,7 @@
|
||||
"shared": "Geteilt",
|
||||
"shared-by": "Geteilt von",
|
||||
"shared-with": "Geteilt mit",
|
||||
"shortest": "Shortest",
|
||||
"shortest": "",
|
||||
"show-in-overview": "In der Übersicht anzeigen",
|
||||
"show-on-map": "Auf der Karte anzeigen",
|
||||
"slogan": "Speichere deine Abenteuer!",
|
||||
@@ -317,14 +339,16 @@
|
||||
"trails-for-you": "Routen für dich",
|
||||
"unchanged": "unverändert",
|
||||
"units": "Einheiten",
|
||||
"unlink": "Link entfernen",
|
||||
"upload-file": "Datei hochladen",
|
||||
"upload-gpx": "GPX hochladen",
|
||||
"upload-new-file": "Neue Datei hochladen",
|
||||
"uploaded": "hochgeladen",
|
||||
"use-hills": "Hügel einbeziehen",
|
||||
"use-hills": "Nutze Berge",
|
||||
"use-roads": "Nutze Straßen",
|
||||
"username": "Nutzername",
|
||||
"view": "Ansehen",
|
||||
"visibilty-status": "Sichtbarkeit",
|
||||
"walking-speed": "Laufgeschwindigkeit",
|
||||
"waypoints": "{n, plural, =1 {Wegpunkt} other {Wegpunkte}}",
|
||||
"welcome_to": "Willkommen bei",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Delete",
|
||||
"delete-account": "Delete Account",
|
||||
"delete-list-confirm": "Do you really want to delete this list? The trails in the list will still be available.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Do you really want to delete this trail? This action cannot be undone.",
|
||||
"describe-your-trail": "Describe your trail",
|
||||
"description": "Description",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Error during login",
|
||||
"error-during-password-reset": "Unable to send password reset email",
|
||||
"error-exporting-trail": "Error exporting trail",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-posting-comment": "Error posting comment",
|
||||
"error-printing-map": "Error printing map",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Focus map on",
|
||||
"follow": "Follow",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"forgot-your-password": "Forgot your password?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "German",
|
||||
"get-position-from-exif": "Get coordinates from EXIF data",
|
||||
"grid": "Grid",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "Help",
|
||||
"hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "Import",
|
||||
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
||||
"include-description": "Include description",
|
||||
"include-waypoints": "Include waypoints",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Language",
|
||||
"latitude": "Latitude",
|
||||
"license": "License",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link copied!",
|
||||
"list": "{n, plural, =1 {List} other {Lists}}",
|
||||
"list-not-shared": "Not shared with anyone",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Not a valid email address",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-completed": "Not completed",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} created a new list",
|
||||
"notification-list-share": "{user} shared a list with you",
|
||||
"notification-new-follower": "You have a new follower",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} left a comment on your trail \"{trail}\"",
|
||||
"notification-trail-create": "{user} created a new trail",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} shared a trail with you",
|
||||
"notifications": "Notifications",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Off",
|
||||
"only-me": "Only me",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "or",
|
||||
"orientation": "Orientation",
|
||||
"paper-size": "Paper size",
|
||||
"paragraph": "",
|
||||
"password": "Password",
|
||||
"password-confirm": "Confirm password",
|
||||
"password-reset-sent": "A password reset email has been sent",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Radius",
|
||||
"read-more": "Read more",
|
||||
"register": "Register",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Removed trail from",
|
||||
"required": "Required",
|
||||
"reset-password": "Reset Password",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Search trails",
|
||||
"select-list": "Select List",
|
||||
"settings": "Settings",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
"settings-notification-list-share": "Someone shared a list with you",
|
||||
"settings-notification-new-follower": "You have a new follower",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Someone left a comment on your trail",
|
||||
"settings-notification-trail-create": "A user who you follow has created a trail",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Someone shared a trail with you",
|
||||
"settings-privacy-account-private": "Only you can see your profile. You will not appear in search results. Other users cannot follow you or share trails with you. You can still publish trails or lists.",
|
||||
"settings-privacy-account-public": "Everyone can see your profile. You appear in search results. Other users can follow you and share trails with you.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "Trails for you",
|
||||
"unchanged": "unchanged",
|
||||
"units": "Units",
|
||||
"unlink": "",
|
||||
"upload-file": "Upload file",
|
||||
"upload-gpx": "Upload GPX",
|
||||
"upload-new-file": "Upload new file",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "Username",
|
||||
"view": "View",
|
||||
"visibilty-status": "Visibility status",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
|
||||
"welcome_to": "Welcome to",
|
||||
|
||||
@@ -23,16 +23,16 @@
|
||||
"author": "Autor",
|
||||
"avatar": "Avatar",
|
||||
"average-speed": "Vel. Promedio",
|
||||
"avoid-bad-surfaces": "Avoid Bad Surfaces",
|
||||
"avoid-bad-surfaces": "Evitar superficies malas",
|
||||
"back": "Atrás",
|
||||
"back-to-login": "Atrás al acceso",
|
||||
"basic-info": "Información básica",
|
||||
"before": "Antes",
|
||||
"bike-type": "Bike Type",
|
||||
"bike-type": "Tipo de bici",
|
||||
"by": "de",
|
||||
"can": "puede",
|
||||
"cancel": "Borrar",
|
||||
"car": "Car",
|
||||
"car": "Coche",
|
||||
"card": "{n, plural, one {}=1 {Ficha} other {Fichas}}",
|
||||
"categories": "Categorías",
|
||||
"category": "Categoría",
|
||||
@@ -41,27 +41,27 @@
|
||||
"change-password": "Cambiar password",
|
||||
"changelog": "Registro de cambios",
|
||||
"chinese": "Chinés (simplificado)",
|
||||
"clear-all": "Clear all",
|
||||
"clear-all": "Limpiar todo",
|
||||
"close": "Cerrar",
|
||||
"comment": "{n, plural, one {}=1 {Comentario} other {Comentarios}}",
|
||||
"completed": "Completado",
|
||||
"completed-a-trail": "ruta completada",
|
||||
"completed-tours": "Completed tours",
|
||||
"completed-tours": "Tracks completados",
|
||||
"completion-status": "Estado de avance",
|
||||
"confirm": "Confirmar",
|
||||
"confirm-deletion": "Confirmar Borrado",
|
||||
"confirm-publish": "Confirmar publicación",
|
||||
"confirm-share": "Confirmar compartir",
|
||||
"connect": "Connect",
|
||||
"connect": "Conectar",
|
||||
"contribute": "Contribuir",
|
||||
"copy-link": "Copiar Enlace",
|
||||
"create-new-list": "Crear una nueva lista",
|
||||
"create-waypoint": "Create waypoint",
|
||||
"create-waypoint": "Crear punto de ruta",
|
||||
"creation-date": "Fecha de creación",
|
||||
"cross": "Cross",
|
||||
"cross": "Cruzar",
|
||||
"current-password": "Contraseña actual",
|
||||
"cycling": "Ciclismo",
|
||||
"cycling-speed": "Cycling Speed",
|
||||
"cycling-speed": "Velocidad de ciclismo",
|
||||
"danger-zone": "Zona peligrosa",
|
||||
"date": "Fecha",
|
||||
"default-category": "Categoría predefinida",
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Borrar",
|
||||
"delete-account": "Borrar Cuenta",
|
||||
"delete-list-confirm": "¿Seguro quieres borrar esta lista? Las rutas en la lista seguirán estando disponibles.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "¿Seguro que quieres borrar esta ruta? Esta acción no puede restablecerse.",
|
||||
"describe-your-trail": "Describe tu ruta",
|
||||
"description": "Descripción",
|
||||
@@ -83,33 +84,34 @@
|
||||
"download": "Descarga",
|
||||
"draw-a-route": "Dibujar una ruta",
|
||||
"driving": "Conducir",
|
||||
"duplicate": "Duplicate",
|
||||
"duplicate": "Duplicar",
|
||||
"duration": "Duración",
|
||||
"dutch": "Holandés",
|
||||
"easy": "Fácil",
|
||||
"edit": "Editar",
|
||||
"edit-entry": "Editar entrada",
|
||||
"edit-list": "Editar Lista",
|
||||
"edit-route": "Edit route",
|
||||
"edit-route": "Editar ruta",
|
||||
"edit-waypoint": "Editar Punto de Interés",
|
||||
"edited": "editado",
|
||||
"elevation-gain": "Ascenso",
|
||||
"elevation-loss": "Descenso",
|
||||
"email": "Correo electrónico",
|
||||
"email-updated": "Correo electrónico actualizado",
|
||||
"email-verified": "Email verified",
|
||||
"email-verified": "Correo verificado",
|
||||
"empty-activities": "{username} todavía no tiene actividad",
|
||||
"empty-bio": "{username} no ha añadido ninguna Bio todavía",
|
||||
"empty-lists": "{username} no tiene listas públicas",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Habilitar auto-ruta",
|
||||
"english": "Inglés",
|
||||
"entry": "Entrada",
|
||||
"error-creating-user": "Error creando el usuario",
|
||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||
"error-disabling-strava-integration": "Error deshabilitando la integración con strava",
|
||||
"error-during-login": "Error durante el acceso",
|
||||
"error-during-password-reset": "Imposible enviar la contraseña de restablecimiento al correo electrónico",
|
||||
"error-exporting-trail": "Error exportando la ruta",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Error al iniciar sesión en komoot",
|
||||
"error-posting-comment": "Error publicando el comentario",
|
||||
"error-printing-map": "Error durante la impresión del mapa",
|
||||
"error-reading-file": "Error leyendo el archivo",
|
||||
@@ -117,7 +119,7 @@
|
||||
"error-saving-trail": "Error guardando la ruta",
|
||||
"error-setting-up-integration": "Error setting up strava integration",
|
||||
"error-updating-password": "Error actualizando la contraseña",
|
||||
"error-updating-strava-integration": "Error updating komoot integration",
|
||||
"error-updating-strava-integration": "Error al actualizar la integración con komoot",
|
||||
"est-duration": "Duración estimada",
|
||||
"explore": "Explora",
|
||||
"explore-some-trails": "Explora alguna ruta",
|
||||
@@ -129,22 +131,24 @@
|
||||
"file-too-big": "Archivo {file} es demasiado grande (max. {size})",
|
||||
"filter-categories": "Filtrar categorías",
|
||||
"filter-difficulty": "Filtrar dificultad",
|
||||
"filter-tags": "Filter tags",
|
||||
"finish": "Finish",
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"filter-tags": "Filtrar etiquetas",
|
||||
"finish": "Finalizar",
|
||||
"fixed-speed": "Velocidad fijada",
|
||||
"focus-map-on": "Centrar mapa sobre",
|
||||
"follow": "Seguir",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Seguidores",
|
||||
"following": "Siguiendo",
|
||||
"forgot-your-password": "¿Contraseña olvidada?",
|
||||
"french": "Francés",
|
||||
"from-file": "From file",
|
||||
"from-photos": "From Photos",
|
||||
"from-url": "From URL",
|
||||
"from-file": "Desde archivo",
|
||||
"from-photos": "Desde fotos",
|
||||
"from-url": "Desde URL",
|
||||
"german": "Alemán",
|
||||
"get-position-from-exif": "Obtener las coordenadas de los datos EXIF",
|
||||
"grid": "Cuadricula",
|
||||
"height": "Height",
|
||||
"heading": "",
|
||||
"height": "Altura",
|
||||
"help": "Ayuda",
|
||||
"hero_section_0_text": "Explorar rutas emocionantes, guarda tus favoritas y disfruta de la belleza de la natura. ¡Encuentra tu próxima aventura!",
|
||||
"hero_section_1_heading": "Parece que todavía no hay rutas.",
|
||||
@@ -152,19 +156,20 @@
|
||||
"hero_section_1_text_alternative": "Empieza guardando tu última aventuras.",
|
||||
"hero_section_2_text": "¿Lo sabías? No solo puedes guardar tus rutas de senderismo. Hay muchas categorías para todas tus aventuras.",
|
||||
"hiking": "Senderismo",
|
||||
"home": "Home",
|
||||
"home": "Inicio",
|
||||
"hungarian": "Húngaro",
|
||||
"hybrid": "Hybrid",
|
||||
"hybrid": "Hibrido",
|
||||
"icon": "Icono",
|
||||
"imperial": "Imperial",
|
||||
"import": "Importar",
|
||||
"import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...",
|
||||
"include-description": "Incluir descripción",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
"integration-enabled": "integration enabled",
|
||||
"integrations": "Integrations",
|
||||
"include-waypoints": "Incluir puntos de interés",
|
||||
"integration-description-komoot": "Sincroniza tus recorridos de Komoot con Wanderer en intervalos regulares.",
|
||||
"integration-description-strava": "Sincroniza tus recorridos de Strava con Wanderer en intervalos regulares.",
|
||||
"integration-disabled": "integración deshabilitada",
|
||||
"integration-enabled": "integración habilitada",
|
||||
"integrations": "Integraciones",
|
||||
"invalid-date": "Fecha no válida",
|
||||
"invalid-username": "Usuario no válido",
|
||||
"italian": "Italiano",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Idioma",
|
||||
"latitude": "Latitud",
|
||||
"license": "Licencia",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "¡Enlace copiado!",
|
||||
"list": "{n, plural, one {}=1 {Lista} other {Listas}}",
|
||||
"list-not-shared": "No compartido con ninguno",
|
||||
@@ -184,14 +192,14 @@
|
||||
"login-details": "Detalles de acceso",
|
||||
"logout": "Salir",
|
||||
"longitude": "Longitud",
|
||||
"loop": "Loop",
|
||||
"loop": "Bucle",
|
||||
"make-one": "¡Crea uno!",
|
||||
"make-thumbnail": "Generar miniaturas",
|
||||
"map": "Mapa",
|
||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||
"metric": "Métrica",
|
||||
"moderate": "Medio",
|
||||
"mountain": "Mountain",
|
||||
"mountain": "Montaña",
|
||||
"must-be-at-least-n-characters-long": "Tiene que tener por lo menos {n} caracteres",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "Mi cuenta",
|
||||
@@ -222,20 +230,28 @@
|
||||
"no-routes-added": "Ninguna ruta añadida",
|
||||
"no-waypoints-yet": "No Puntos de Interés todavía",
|
||||
"not-a-valid-email-address": "Correo electrónico no válido",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-a-valid-url": "No es un URL válida",
|
||||
"not-completed": "No completado",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} ha creado una nueva lista",
|
||||
"notification-list-share": "{user} ha compartido una nueva lista contigo",
|
||||
"notification-new-follower": "Tienes un nuevo seguidor",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} ha dejado un comentario sobre tu ruta \"{trail}\"",
|
||||
"notification-trail-create": "{user} ha creado una nueva ruta",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} ha compartido una ruta contigo",
|
||||
"notifications": "Notificaciones",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Apagado",
|
||||
"only-me": "Solo yo",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "o",
|
||||
"orientation": "Orientación",
|
||||
"paper-size": "Tamaño del papel",
|
||||
"paragraph": "",
|
||||
"password": "Contraseña",
|
||||
"password-confirm": "Confirmar contraseña",
|
||||
"password-reset-sent": "Correo electrónico para restablecer la contraseña enviado",
|
||||
@@ -256,10 +272,11 @@
|
||||
"radius": "Radio",
|
||||
"read-more": "Leer más",
|
||||
"register": "Registrar",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Ruta borrada de",
|
||||
"required": "Obligatorio",
|
||||
"reset-password": "Restablecer Contraseña",
|
||||
"road": "Road",
|
||||
"road": "Carretera",
|
||||
"route": "{n, plural, =1 {Route} other {Routes}}",
|
||||
"route-point": "Route Point",
|
||||
"save": "Guardar",
|
||||
@@ -268,15 +285,20 @@
|
||||
"save-your-trail-first": "Guarda tu ruta primero",
|
||||
"search-cities": "Buscar ciudades",
|
||||
"search-for-trails-places": "Busca rutas, lugares",
|
||||
"search-places": "Search places",
|
||||
"search-places": "Buscar lugares",
|
||||
"search-trails": "Buscar ruta",
|
||||
"select-list": "Seleccionar Lista",
|
||||
"settings": "Configuración",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un usuario al que sigues ha creado una nueva lista",
|
||||
"settings-notification-list-share": "Alguien ha compartido una lista contigo",
|
||||
"settings-notification-new-follower": "Tienes un nuevo seguidor",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Alguien ha dejado un comentario sobre tu ruta",
|
||||
"settings-notification-trail-create": "Un usuario al que sigues ha creado una nueva ruta",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Alguien ha compartido una ruta contigo",
|
||||
"settings-privacy-account-private": "Solo tu puedes ver tu perfil. No aparecerás en los resultados de búsquedas. Otros usuarios no pueden seguirte o compartir rutas contigo. Todavía sí puedes publicar tus rutas o listas.",
|
||||
"settings-privacy-account-public": "Todos pueden ver tu perfil. Aparecerás en los resultados de búsquedas. Otros usuarios pueden seguirte y compartir contigo rutas.",
|
||||
@@ -284,7 +306,7 @@
|
||||
"settings-privacy-lists-public": "Tus listas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para listas específicas.",
|
||||
"settings-privacy-trails-private": "Tus rutas son privadas por defecto. Nadie excepto tú podrás verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
|
||||
"settings-privacy-trails-public": "Tus rutas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
|
||||
"settings-saved": "Settings saved",
|
||||
"settings-saved": "Configuración guardada",
|
||||
"share": "Compartir",
|
||||
"share-profile": "Compartir perfil",
|
||||
"share-this-list": "Compartir esta lista",
|
||||
@@ -301,13 +323,13 @@
|
||||
"sort": "Ordenar",
|
||||
"spanish": "Español",
|
||||
"speed": "Velocidad",
|
||||
"start": "Start",
|
||||
"start": "Empezar",
|
||||
"statistics": "Estadísticas",
|
||||
"stop-drawing": "Parar de diseñar",
|
||||
"stop-editing": "Stop editing",
|
||||
"stop-editing": "Parar de editar",
|
||||
"summit-book": "Libro de ascensos",
|
||||
"table": "Tabla",
|
||||
"tags": "Tags",
|
||||
"tags": "Etiquetas",
|
||||
"text": "Texto",
|
||||
"tilesets": "Ficha personalizada",
|
||||
"top-speed": "Top Speed",
|
||||
@@ -317,18 +339,20 @@
|
||||
"trails-for-you": "Rutas para ti",
|
||||
"unchanged": "unchanged",
|
||||
"units": "Unidades",
|
||||
"unlink": "",
|
||||
"upload-file": "Cargar archivo",
|
||||
"upload-gpx": "Cargar GPX",
|
||||
"upload-new-file": "Upload new file",
|
||||
"upload-new-file": "Subir nuevo archivo",
|
||||
"uploaded": "cargado",
|
||||
"use-hills": "",
|
||||
"use-roads": "Use Roads",
|
||||
"use-roads": "Utilizar carreteras",
|
||||
"username": "Nombre de usuario",
|
||||
"view": "Ver",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, one {}=1 {Punto de Interés} other {Puntos de Interés}}",
|
||||
"welcome_to": "Bienvenid@ a",
|
||||
"width": "Width",
|
||||
"width": "Anchura",
|
||||
"wrong-username-or-password": "Usuario o contraseña no correctas",
|
||||
"you-can": "Puedes"
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Supprimer",
|
||||
"delete-account": "Supprimer le compte",
|
||||
"delete-list-confirm": "Voulez-vous vraiment supprimer cette liste ? Les itinéraires de la liste seront toujours disponibles.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Voulez-vous vraiment supprimer cet itinéraire ? Cette action ne peut être annulée.",
|
||||
"describe-your-trail": "Décrivez votre itinéraire",
|
||||
"description": "Description",
|
||||
@@ -101,7 +102,7 @@
|
||||
"empty-activities": "{username} n'a encore aucune activité",
|
||||
"empty-bio": "{username} n'a pas encore de description",
|
||||
"empty-lists": "{username} n'a pas de liste publique",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Anglais",
|
||||
"entry": "Entrée",
|
||||
"error-creating-user": "Erreur durant la création de l'utilisateur",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Erreur durant la connexion",
|
||||
"error-during-password-reset": "Impossible d'envoyer l'e-mail de réinitialisation du mot de passe",
|
||||
"error-exporting-trail": "Erreur lors de l'export de l'itinéraire",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Erreur de connexion à Komoot",
|
||||
"error-posting-comment": "Erreur lors de la publication du commentaire",
|
||||
"error-printing-map": "Erreur d'impression de la carte",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Centrer la carte sur",
|
||||
"follow": "Suivre",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Abonné(e)s",
|
||||
"following": "Abonnements",
|
||||
"forgot-your-password": "Mot de passe oublié ?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "Allemand",
|
||||
"get-position-from-exif": "Obtenir les coordonnées à partir des données EXIF",
|
||||
"grid": "Grille",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "Aide",
|
||||
"hero_section_0_text": "Explorez des itinéraires passionnants, sauvegardez vos favoris et découvrez la beauté de la nature. Trouvez votre prochaine aventure !",
|
||||
@@ -160,8 +164,9 @@
|
||||
"import": "Importer",
|
||||
"import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...",
|
||||
"include-description": "Inclure la description",
|
||||
"integration-description-komoot": "Synchronisez vos Tours Komoot avec Wanderer à intervalles réguliers.",
|
||||
"integration-description-strava": "Synchronisez vos itinéraires et vos activités Strava avec Wanderer à intervalles réguliers.",
|
||||
"include-waypoints": "Inclure les points de passage",
|
||||
"integration-description-komoot": "Synchronisez vos Tours Komoot avec wanderer à intervalles réguliers.",
|
||||
"integration-description-strava": "Synchronisez vos itinéraires et vos activités Strava avec wanderer à intervalles réguliers.",
|
||||
"integration-disabled": "Intégration désactivée",
|
||||
"integration-enabled": "Intégration activée",
|
||||
"integrations": "Intégrations",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Langue",
|
||||
"latitude": "Latitude",
|
||||
"license": "Licence",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Lien copié !",
|
||||
"list": "{n, plural, =1 {Liste} other {Listes}}",
|
||||
"list-not-shared": "Non partagé avec quiconque",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Adresse email invalide",
|
||||
"not-a-valid-url": "URL non valide",
|
||||
"not-completed": "Non terminés",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} a créé une nouvelle liste",
|
||||
"notification-list-share": "{user} à partagé une liste avec vous",
|
||||
"notification-new-follower": "Vous avez un nouvel abonné",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} a laissé un commentaire sur votre itinéraire {trail}",
|
||||
"notification-trail-create": "{user} a créé un nouvel itinéraire",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} a partagé un itinéraire avec vous",
|
||||
"notifications": "Notifications",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Désactivé",
|
||||
"only-me": "Seulement moi",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "ou",
|
||||
"orientation": "Orientation",
|
||||
"paper-size": "Taille de papier",
|
||||
"paragraph": "",
|
||||
"password": "Mot de passe",
|
||||
"password-confirm": "Confirmer le mot de passe",
|
||||
"password-reset-sent": "Un e-mail de réinitialisation de votre mot de passe a été envoyé",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Rayon",
|
||||
"read-more": "Voir plus",
|
||||
"register": "Créer un compte",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Enlever l'itinéraire de",
|
||||
"required": "Requis",
|
||||
"reset-password": "Réinitialiser le mot de passe",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Chercher un itinéraire",
|
||||
"select-list": "Liste de choix",
|
||||
"settings": "Paramètres",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un utilisateur que vous suivez à créé une nouvelle liste",
|
||||
"settings-notification-list-share": "Quelqu'un à partagé une liste avec vous",
|
||||
"settings-notification-new-follower": "Vous avez un nouvel abonné",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Quelqu'un a commenté votre itinéraire",
|
||||
"settings-notification-trail-create": "Un utilisateur que vous suivez a créé un nouvel itinéraire",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Quelqu'un a partagé un itinéraire avec vous",
|
||||
"settings-privacy-account-private": "Vous seul pouvez voir votre profil. Vous n'apparaîtrez pas dans les résultats de recherche. Les autres utilisateurs ne pourront pas vous suivre ni partager d'itinéraire avec vous. Vous pourrez tout de même publier des itinéraires ou des listes.",
|
||||
"settings-privacy-account-public": "Tout le monde peut voir votre profil. Vous apparaîtrez dans les résultats de recherche. Les autres utilisateurs pourront vous suivre et partager des itinéraires avec vous.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "Itinéraire pour vous",
|
||||
"unchanged": "pas de modification",
|
||||
"units": "Unités",
|
||||
"unlink": "",
|
||||
"upload-file": "Importer un fichier",
|
||||
"upload-gpx": "Envoyer un GPX",
|
||||
"upload-new-file": "Importer un fichier",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "Nom d'utilisateur",
|
||||
"view": "Afficher",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Point de passage} other {Points de passage}}",
|
||||
"welcome_to": "Bienvenue sur",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Törlés",
|
||||
"delete-account": "Fiók törlése",
|
||||
"delete-list-confirm": "Tényleg törölni akarja ezt a listát? A listán szereplő nyomvonalak továbbra is elérhetőek maradnak.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Tényleg törölni akarja ezt a nyomvonalat? Ezt a műveletet nem lehet visszavonni!",
|
||||
"describe-your-trail": "Nyomvonal leírása",
|
||||
"description": "Leírás",
|
||||
@@ -101,7 +102,7 @@
|
||||
"empty-activities": "{username} has no activity yet",
|
||||
"empty-bio": "{username} has not added a bio yet",
|
||||
"empty-lists": "{username} has no public lists",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Angol",
|
||||
"entry": "Bejegyzés",
|
||||
"error-creating-user": "Hiba felhasználó hozzáadása közben",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Hiba bejelentkezés közben",
|
||||
"error-during-password-reset": "Unable to send password reset email",
|
||||
"error-exporting-trail": "Error exporting trail",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-posting-comment": "Error posting comment",
|
||||
"error-printing-map": "Error printing map",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Focus map on",
|
||||
"follow": "Follow",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"forgot-your-password": "Forgot your password?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "Német",
|
||||
"get-position-from-exif": "Get coordinates from EXIF data",
|
||||
"grid": "Grid",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "Help",
|
||||
"hero_section_0_text": "Fedezze fel az izgalmas útvonalakat, mentse el kedvenceit, és tapasztalja meg a természet szépségét. Találja meg a következő kalandját!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "Import",
|
||||
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
||||
"include-description": "Include description",
|
||||
"include-waypoints": "Útpontok hozzáadása",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Nyelf",
|
||||
"latitude": "Szélesség",
|
||||
"license": "License",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link copied!",
|
||||
"list": "{n, plural, =1 {Lista} other {Listák}}",
|
||||
"list-not-shared": "Not shared with anyone",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Érvénytelen e-mail cím",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-completed": "Nem teljesített",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} created a new list",
|
||||
"notification-list-share": "{user} shared a list with you",
|
||||
"notification-new-follower": "You have a new follower",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} left a comment on your trail \"{trail}\"",
|
||||
"notification-trail-create": "{user} created a new trail",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} shared a trail with you",
|
||||
"notifications": "Notifications",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Off",
|
||||
"only-me": "Only me",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "vagy",
|
||||
"orientation": "Orientation",
|
||||
"paper-size": "Paper size",
|
||||
"paragraph": "",
|
||||
"password": "Jelszó",
|
||||
"password-confirm": "Confirm password",
|
||||
"password-reset-sent": "A password reset email has been sent",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Átmérő",
|
||||
"read-more": "Read more",
|
||||
"register": "Regisztráció",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Eltávolított nyomvonal a",
|
||||
"required": "Kötelező",
|
||||
"reset-password": "Reset Password",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Nyomvonalak keresése",
|
||||
"select-list": "Lista kiválasztása",
|
||||
"settings": "Beállítások",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
"settings-notification-list-share": "Someone shared a list with you",
|
||||
"settings-notification-new-follower": "You have a new follower",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Someone left a comment on your trail",
|
||||
"settings-notification-trail-create": "A user who you follow has created a trail",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Someone shared a trail with you",
|
||||
"settings-privacy-account-private": "Only you can see your profile. You will not appear in search results. Other users cannot follow you or share trails with you. You can still publish trails or lists.",
|
||||
"settings-privacy-account-public": "Everyone can see your profile. You appear in search results. Other users can follow you and share trails with you.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "Útvonalak önnek",
|
||||
"unchanged": "unchanged",
|
||||
"units": "Mértékegységek",
|
||||
"unlink": "",
|
||||
"upload-file": "Fájl feltöltése",
|
||||
"upload-gpx": "GPX feltöltése",
|
||||
"upload-new-file": "Upload new file",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "Felhasználónév",
|
||||
"view": "View",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
|
||||
"welcome_to": "Üdvözöljük a",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Elimina",
|
||||
"delete-account": "Elimina account",
|
||||
"delete-list-confirm": "Vuoi davvero cancellare questa lista? I percorsi presenti nella lista saranno ancora disponibili.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Vuoi davvero cancellare questo percorso? Questa azione non può essere revocata.",
|
||||
"describe-your-trail": "Descrivi il tuo percorso",
|
||||
"description": "Descrizione",
|
||||
@@ -101,7 +102,7 @@
|
||||
"empty-activities": "{username} non ha ancora attività",
|
||||
"empty-bio": "{username} non ha ancora aggiunto una biografia",
|
||||
"empty-lists": "{username} non ha liste pubbliche",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Inglese",
|
||||
"entry": "Voce",
|
||||
"error-creating-user": "Errore nella creazione dell'utente",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Errore durante il login",
|
||||
"error-during-password-reset": "Impossibile inviare email per ripristinare la password",
|
||||
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-posting-comment": "Errore pubblicando il commento",
|
||||
"error-printing-map": "Errore durante la stampa della mappa",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Focus sulla mappa",
|
||||
"follow": "Seguire",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Seguaci",
|
||||
"following": "Seguendo",
|
||||
"forgot-your-password": "Password dimenticata?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "Tedesco",
|
||||
"get-position-from-exif": "Ottieni posizione da dati EXIF",
|
||||
"grid": "Griglia",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "Aiuto",
|
||||
"hero_section_0_text": "Scopri percorsi emozionanti, salva i tuoi preferiti e vivi la bellezza della natura. Trova la tua prossima avventura!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "Importa",
|
||||
"import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...",
|
||||
"include-description": "Adotta descrizione",
|
||||
"include-waypoints": "Includi waypoint",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Lingua",
|
||||
"latitude": "Latitudine",
|
||||
"license": "Licenza",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link copiato",
|
||||
"list": "{n, plural, =1 {Lista} other {Liste}}",
|
||||
"list-not-shared": "Non condivisa",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Indirizzo email non valido",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-completed": "Non completato",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} ha creato una nuova lista",
|
||||
"notification-list-share": "{user} ha condiviso una lista con te",
|
||||
"notification-new-follower": "Hai un nuovo seguace",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} ha lasciato un commento sul tuo percorso \"{trail}\"",
|
||||
"notification-trail-create": "{user} ha creato un nuovo percorso",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} ha condiviso un percorso con te",
|
||||
"notifications": "Notifiche",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Spento",
|
||||
"only-me": "Solo io",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "o",
|
||||
"orientation": "Orientamento",
|
||||
"paper-size": "Formato carta",
|
||||
"paragraph": "",
|
||||
"password": "Password",
|
||||
"password-confirm": "Confermare password",
|
||||
"password-reset-sent": "Un email per ripristinare la password è stato inviato",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Raggio",
|
||||
"read-more": "Per saperne di più",
|
||||
"register": "Registrati",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Percorso rimosso da",
|
||||
"required": "Obbligatorio",
|
||||
"reset-password": "Ripristinare Password",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Cerca percorsi",
|
||||
"select-list": "Seleziona lista",
|
||||
"settings": "Impostazioni",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un utente che segui ha creato una lista",
|
||||
"settings-notification-list-share": "Qualcuno ha condiviso una lista con te",
|
||||
"settings-notification-new-follower": "Hai un nuovo seguace",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Qualcuno ha lasciato un commento sul tuo percorso",
|
||||
"settings-notification-trail-create": "Un utente che segui ha creato un percorso",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Qualcuno ha condiviso un percorso con te",
|
||||
"settings-privacy-account-private": "Solo tu puoi vedere il tuo profilo. Non apparirai nei risultati della ricerca. Altri utenti non potranno seguirti o condividere i percorsi con te. Tuttavia potrai comunque pubblicare percorsi o liste.",
|
||||
"settings-privacy-account-public": "Tutti possono vedere il tuo profilo. Appare nei risultati della ricerca. Altri utenti possono seguirti e condividere con te i percorsi.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "Percorsi per te",
|
||||
"unchanged": "unchanged",
|
||||
"units": "Unità",
|
||||
"unlink": "",
|
||||
"upload-file": "Carica file",
|
||||
"upload-gpx": "Carica file GPX",
|
||||
"upload-new-file": "Upload new file",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "Nome utente",
|
||||
"view": "Visualizza",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Punto di passaggio} other {Punti di passaggio}}",
|
||||
"welcome_to": "Benvenuti a",
|
||||
|
||||
@@ -6,195 +6,203 @@
|
||||
"Skiing": "Skiën",
|
||||
"Walking": "Wandelen",
|
||||
"about": "Over",
|
||||
"account-delete-confirm": "Je staat op het punt je account te verwijderen. Al je wandelroutes worden hierdoor eveneens verwijderd. Wil je doorgaan?",
|
||||
"account-delete-confirm": "Je staat op het punt je account te verwijderen. Al je routes worden hierdoor eveneens verwijderd. Wil je doorgaan?",
|
||||
"account-privacy": "Account privacy",
|
||||
"activity": "{n, plural, =1 {Activity} other {Activities}}",
|
||||
"add-bio": "Add Bio",
|
||||
"activity": "{n, plural, =1 {Activiteit} other {Activiteiten}}",
|
||||
"add-bio": "Voeg Bio toe",
|
||||
"add-entry": "Item toevoegen",
|
||||
"add-to-list": "Toevoegen aan lijst",
|
||||
"add-waypoint": "Routepunt toevoegen",
|
||||
"added-trail-to": "Wandelroute toegevoegd aan",
|
||||
"after": "After",
|
||||
"all-activities": "All activities",
|
||||
"added-trail-to": "Route toegevoegd aan",
|
||||
"after": "Na",
|
||||
"all-activities": "Alle activiteiten",
|
||||
"alphabetical": "Alfabetisch",
|
||||
"already-account": "Heb je al een account?",
|
||||
"altitude": "Hoogte",
|
||||
"api-documentation": "API-documentatie",
|
||||
"author": "Author",
|
||||
"author": "Auteur",
|
||||
"avatar": "Profielfoto",
|
||||
"average-speed": "Avg. Speed",
|
||||
"avoid-bad-surfaces": "Avoid Bad Surfaces",
|
||||
"back": "Back",
|
||||
"average-speed": "Gem. Snelheid",
|
||||
"avoid-bad-surfaces": "Vermijd slechte ondergrond",
|
||||
"back": "Terug",
|
||||
"back-to-login": "Terug naar login",
|
||||
"basic-info": "Algemene informatie",
|
||||
"before": "Before",
|
||||
"bike-type": "Bike Type",
|
||||
"by": "by",
|
||||
"can": "can",
|
||||
"before": "Voor",
|
||||
"bike-type": "Fiets Type",
|
||||
"by": "door",
|
||||
"can": "kan",
|
||||
"cancel": "Annuleren",
|
||||
"car": "Car",
|
||||
"card": "{n, plural, =1 {kaart} other {kaarten}}",
|
||||
"car": "Wagen",
|
||||
"card": "{n, plural, =1 {Kaart} other {Kaarten}}",
|
||||
"categories": "Categorieën",
|
||||
"category": "Categorie",
|
||||
"change": "Wijzigen",
|
||||
"change-email": "E-mail wijzigen",
|
||||
"change-password": "Wachtwoord wijzigen",
|
||||
"changelog": "Wijzigingslog",
|
||||
"chinese": "Versimpeld Chinees",
|
||||
"clear-all": "Clear all",
|
||||
"chinese": "Chinees (vereenvoudigd)",
|
||||
"clear-all": "Wis alles",
|
||||
"close": "Sluiten",
|
||||
"comment": "{n, plural, =1 {Opmerking} other {Opmerkingen}}",
|
||||
"completed": "Voltooid",
|
||||
"completed-a-trail": "completed a trail",
|
||||
"completed-tours": "Completed tours",
|
||||
"completed-a-trail": "Route voltooid",
|
||||
"completed-tours": "Afgeronde tochten",
|
||||
"completion-status": "Voltooiingsstatus",
|
||||
"confirm": "Confirm",
|
||||
"confirm-deletion": "Confirm Deletion",
|
||||
"confirm-publish": "Confirm publishing",
|
||||
"confirm-share": "Confirm share",
|
||||
"connect": "Connect",
|
||||
"confirm": "Bevestig",
|
||||
"confirm-deletion": "Bevestig verwijderen",
|
||||
"confirm-publish": "Bevestig publicatie",
|
||||
"confirm-share": "Bevestig delen",
|
||||
"connect": "Verbind",
|
||||
"contribute": "Bijdragen",
|
||||
"copy-link": "Copy Link",
|
||||
"copy-link": "Kopieer Link",
|
||||
"create-new-list": "Nieuwe lijst",
|
||||
"create-waypoint": "Create waypoint",
|
||||
"create-waypoint": "Nieuw routepunt",
|
||||
"creation-date": "Aanmaakdatum",
|
||||
"cross": "Cross",
|
||||
"cross": "Oversteken",
|
||||
"current-password": "Huidig wachtwoord",
|
||||
"cycling": "Fietsen",
|
||||
"cycling-speed": "Cycling Speed",
|
||||
"cycling-speed": "Fietssnelheid",
|
||||
"danger-zone": "Gevarenzone",
|
||||
"date": "Datum",
|
||||
"default-category": "Standaardcategorie",
|
||||
"default-location": "Standaardlocatie",
|
||||
"degrees": "Degrees",
|
||||
"degrees": "Graden",
|
||||
"delete": "Verwijderen",
|
||||
"delete-account": "Account verwijderen",
|
||||
"delete-list-confirm": "Weet je zeker dat je deze lijst wilt verwijderen? De wandelroutes worden bewaard.",
|
||||
"delete-trail-confirm": "Weet je zeker dat je deze wandelroute wilt verwijderen? Deze actie is onomkeerbaar.",
|
||||
"describe-your-trail": "Omschrijf je route",
|
||||
"delete-list-confirm": "Weet je zeker dat je deze lijst wilt verwijderen? De routes worden bewaard.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Weet je zeker dat je deze route wilt verwijderen? Deze actie is onomkeerbaar.",
|
||||
"describe-your-trail": "Omschrijf je wandelroute",
|
||||
"description": "Omschrijving",
|
||||
"difficult": "Moeilijk",
|
||||
"difficulty": "Moeilijkheidsgraad",
|
||||
"directions": "Routebeschrijving",
|
||||
"display": "Display",
|
||||
"display": "Tonen",
|
||||
"display-as": "Tonen als",
|
||||
"distance": "Afstand",
|
||||
"documentation": "Documentatie",
|
||||
"download": "Download",
|
||||
"draw-a-route": "Draw a route",
|
||||
"driving": "Driving",
|
||||
"duplicate": "Duplicate",
|
||||
"duration": "Duration",
|
||||
"draw-a-route": "Teken een route",
|
||||
"driving": "Rijden",
|
||||
"duplicate": "Dupliceer",
|
||||
"duration": "Duur",
|
||||
"dutch": "Nederlands",
|
||||
"easy": "Makkelijk",
|
||||
"edit": "Bewerken",
|
||||
"edit-entry": "Item bewerken",
|
||||
"edit-list": "Lijst aanpassen",
|
||||
"edit-route": "Edit route",
|
||||
"edit-waypoint": "Routepunt aanpassen",
|
||||
"edited": "edited",
|
||||
"edit": "Bewerk",
|
||||
"edit-entry": "Bewerk Item",
|
||||
"edit-list": "Bewerk Lijst",
|
||||
"edit-route": "Bewerk route",
|
||||
"edit-waypoint": "Bewerk Routepunt",
|
||||
"edited": "bewerkt",
|
||||
"elevation-gain": "Hoogteverschil",
|
||||
"elevation-loss": "Elevation Loss",
|
||||
"elevation-loss": "Hoogteverlies",
|
||||
"email": "E-mail",
|
||||
"email-updated": "Email updated",
|
||||
"email-verified": "Email verified",
|
||||
"empty-activities": "{username} has no activity yet",
|
||||
"empty-bio": "{username} has not added a bio yet",
|
||||
"empty-lists": "{username} has no public lists",
|
||||
"enable-auto-routing": "",
|
||||
"email-updated": "Email bijgewerkt",
|
||||
"email-verified": "Email geverifieerd",
|
||||
"empty-activities": "{username} heeft nog geen activiteit",
|
||||
"empty-bio": "{username} heeft nog geen bio toegevoegd",
|
||||
"empty-lists": "{username} heeft geen publieke lijsten",
|
||||
"enable-auto-routing": "Automatische routering inschakelen",
|
||||
"english": "Engels",
|
||||
"entry": "Item",
|
||||
"error-creating-user": "Het account kan niet worden aangemaakt",
|
||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||
"error-creating-user": "Fout bij aanmaken gebruiker",
|
||||
"error-disabling-strava-integration": "Fout bij het uitschakelen van Strava-integratie",
|
||||
"error-during-login": "Het inloggen is mislukt",
|
||||
"error-during-password-reset": "Unable to send password reset email",
|
||||
"error-exporting-trail": "Error exporting trail",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-posting-comment": "Error posting comment",
|
||||
"error-during-password-reset": "Kan geen e-mail voor wachtwoordherstel verzenden",
|
||||
"error-exporting-trail": "Fout bij exporteren van parcours",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Fout tijdens inloggen in Komoot",
|
||||
"error-posting-comment": "Fout bij het plaatsen van een reactie",
|
||||
"error-printing-map": "Fout bij afdrukken van kaart",
|
||||
"error-reading-file": "Het bestand kan niet worden ingelezen",
|
||||
"error-saving-list": "Error saving list",
|
||||
"error-saving-trail": "Error saving trail",
|
||||
"error-setting-up-integration": "Error setting up strava integration",
|
||||
"error-updating-password": "Error updating password",
|
||||
"error-updating-strava-integration": "Error updating komoot integration",
|
||||
"error-reading-file": "Fout bij inlezen bestand",
|
||||
"error-saving-list": "Fout bij bewaren van lijst",
|
||||
"error-saving-trail": "Fout bij bewaren van route",
|
||||
"error-setting-up-integration": "Fout bij opzetten {provider} integratie",
|
||||
"error-updating-password": "Fout bij bijwerken van wachtwoord",
|
||||
"error-updating-strava-integration": "Fout bij bijwerken van Komoot integratie",
|
||||
"est-duration": "Geschatte duur",
|
||||
"explore": "Verkennen",
|
||||
"explore-some-trails": "Verken enkele wandelroutes",
|
||||
"export": "Export",
|
||||
"export-all-trails": "Export all trails",
|
||||
"favourite-sport": "Favourite sport",
|
||||
"explore-some-trails": "Verken enkele routes",
|
||||
"export": "Exporteer",
|
||||
"export-all-trails": "Exporteer alle routes",
|
||||
"favourite-sport": "Favoriete sport",
|
||||
"features": "Kenmerken",
|
||||
"file-format": "File format",
|
||||
"file-too-big": "File {file} is too big (max. {size})",
|
||||
"filter-categories": "Filter categories",
|
||||
"filter-difficulty": "Filter difficulty",
|
||||
"file-format": "Bestandsformaat",
|
||||
"file-too-big": "Bestand {file} is te groot (max. {size})",
|
||||
"filter-categories": "Filter categorieën",
|
||||
"filter-difficulty": "Filter moeilijkheid",
|
||||
"filter-tags": "Filter tags",
|
||||
"finish": "Finish",
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Focus map on",
|
||||
"follow": "Follow",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"forgot-your-password": "Forgot your password?",
|
||||
"fixed-speed": "Vaste snelheid",
|
||||
"focus-map-on": "Focus kaart op",
|
||||
"follow": "Volg",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Volgers",
|
||||
"following": "Volgend",
|
||||
"forgot-your-password": "Wachtwoord vergeten?",
|
||||
"french": "Frans",
|
||||
"from-file": "From file",
|
||||
"from-photos": "From Photos",
|
||||
"from-url": "From URL",
|
||||
"from-file": "Van bestand",
|
||||
"from-photos": "Van Foto's",
|
||||
"from-url": "Van URL",
|
||||
"german": "Duits",
|
||||
"get-position-from-exif": "Get coordinates from EXIF data",
|
||||
"get-position-from-exif": "Coördinaten ophalen uit EXIF-gegevens",
|
||||
"grid": "Rooster",
|
||||
"height": "Height",
|
||||
"heading": "",
|
||||
"height": "Hoogte",
|
||||
"help": "Help",
|
||||
"hero_section_0_text": "Verken uitdagende wandelroutes, bewaar je favoriete en ervaar de pracht van de natuur. Op naar het volgende avontuur!",
|
||||
"hero_section_1_heading": "Je hebt nog geen wandelroutes.",
|
||||
"hero_section_0_text": "Verken uitdagende routes, bewaar je favoriete en ervaar de pracht van de natuur. Op naar het volgende avontuur!",
|
||||
"hero_section_1_heading": "Je hebt nog geen routes.",
|
||||
"hero_section_1_text": "Wellicht vind je deze routes interessant. Ook kun je de volledige lijst bekijken.",
|
||||
"hero_section_1_text_alternative": "Bewaar je recentste avontuur om aan de slag te gaan.",
|
||||
"hero_section_2_text": "Wist je dat…? Je kunt niet alleen wandelroutes opslaan: er zijn ook categorieën voor andere avonturen.",
|
||||
"hero_section_2_text": "Wist je dat…? Je kunt niet alleen hiking routes opslaan: er zijn ook veel categorieën voor al je andere avonturen.",
|
||||
"hiking": "Hiking",
|
||||
"home": "Home",
|
||||
"hungarian": "Hongaars",
|
||||
"hybrid": "Hybrid",
|
||||
"hybrid": "Hybride",
|
||||
"icon": "Pictogram",
|
||||
"imperial": "Imperiaal",
|
||||
"import": "Import",
|
||||
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
||||
"import": "Importeer",
|
||||
"import-hint": "Selecteer of sleep GPX-, FIT-, KML- of TCX-bestanden hierheen...",
|
||||
"include-description": "Inclusief beschrijving",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
"integration-enabled": "integration enabled",
|
||||
"integrations": "Integrations",
|
||||
"include-waypoints": "Waypoints toevoegen",
|
||||
"integration-description-komoot": "Synchroniseert je Komoot-tochten met Wanderer op regelmatige tijdstippen.",
|
||||
"integration-description-strava": "Synchroniseert je Strava-routes en -activiteiten met Wanderer op regelmatige tijdstippen.",
|
||||
"integration-disabled": "Integratie uitgeschakeld",
|
||||
"integration-enabled": "Integratie ingeschakeld",
|
||||
"integrations": "Integraties",
|
||||
"invalid-date": "Ongeldige datum",
|
||||
"invalid-username": "Ongeldige gebruikersnaam",
|
||||
"italian": "Italiaans",
|
||||
"joined": "Joined",
|
||||
"joined": "Aangesloten",
|
||||
"language": "Taal",
|
||||
"latitude": "Breedtegraad",
|
||||
"license": "Licentie",
|
||||
"link-copied": "Link copied!",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link gekopieerd",
|
||||
"list": "{n, plural, =1 {Lijst} other {Lijsten}}",
|
||||
"list-not-shared": "Not shared with anyone",
|
||||
"list-public-warning": "All trails in this list will become public.",
|
||||
"list-saved-successfully": "List saved successfully",
|
||||
"list-share-warning": "Sharing a list automatically shares all trails contained in it.",
|
||||
"list-share-warning-update": "Added trails will be shared with everyone that has access to this list.",
|
||||
"list-not-shared": "Niet gedeeld met iemand",
|
||||
"list-public-warning": "Alle routes in deze lijst worden publiek",
|
||||
"list-saved-successfully": "Lijst succesvol bewaard",
|
||||
"list-share-warning": "Als u een lijst deelt, worden automatisch ook alle routes die erin voorkomen, gedeeld.",
|
||||
"list-share-warning-update": "Toegevoegde routes worden gedeeld met iedereen die toegang heeft tot deze lijst.",
|
||||
"location": "Locatie",
|
||||
"login": "Inloggen",
|
||||
"login-details": "Login details",
|
||||
"logout": "Uitloggen",
|
||||
"longitude": "Lengtegraad",
|
||||
"loop": "Loop",
|
||||
"loop": "Lus",
|
||||
"make-one": "Maak er een aan!",
|
||||
"make-thumbnail": "Miniatuur maken",
|
||||
"map": "Kaart",
|
||||
"max-hiking-difficulty": "Max. Hiking Difficulty",
|
||||
"max-hiking-difficulty": "Max. Hiking Moeilijkheid",
|
||||
"metric": "Metrisch",
|
||||
"moderate": "Gemiddeld",
|
||||
"mountain": "Mountain",
|
||||
"mountain": "Berg",
|
||||
"must-be-at-least-n-characters-long": "Minimaal {n} tekens",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "My Account",
|
||||
"must-be-at-most-n-characters-long": "Mag maximaal {n} tekens lang zijn.",
|
||||
"my-account": "Mijn Account",
|
||||
"n-days-ago": "{n} dagen geleden",
|
||||
"n-hours-ago": "{n} uuren geleden",
|
||||
"n-minutes-ago": "{n} minuten geleden",
|
||||
@@ -204,131 +212,147 @@
|
||||
"name": "Naam",
|
||||
"near": "Nabij",
|
||||
"new-list": "Nieuwe lijst",
|
||||
"new-password": "New password",
|
||||
"new-password-error": "Error setting new password",
|
||||
"new-password-success": "The new password has been set",
|
||||
"new-password-text": "Set a new password",
|
||||
"new-trail": "Nieuwe wandelroute",
|
||||
"new-password": "Nieuw wachtwoord",
|
||||
"new-password-error": "Fout bij het instellen van een nieuw wachtwoord",
|
||||
"new-password-success": "Het nieuwe wachtwoord is ingesteld.",
|
||||
"new-password-text": "Stel nieuw wachtwoord in",
|
||||
"new-trail": "Nieuwe Route",
|
||||
"no-account": "Heb je nog geen account?",
|
||||
"no-comments-so-far": "No comments so far",
|
||||
"no-data": "No data",
|
||||
"no-description-for-now": "No description for now",
|
||||
"no-comments-so-far": "Tot nu toe geen opmerkingen",
|
||||
"no-data": "Geen data",
|
||||
"no-description-for-now": "Voorlopig geen beschrijving",
|
||||
"no-gps-data-in-image": "",
|
||||
"no-grid": "No Grid",
|
||||
"no-notifications": "No notifications",
|
||||
"no-grid": "Geen raster",
|
||||
"no-notifications": "Geen meldingen",
|
||||
"no-photos-here": "No photos here",
|
||||
"no-preference": "Geen voorkeur",
|
||||
"no-results": "Er zijn geen zoekresultaten",
|
||||
"no-routes-added": "No routes added",
|
||||
"no-waypoints-yet": "No waypoints yet",
|
||||
"no-routes-added": "Geen ",
|
||||
"no-waypoints-yet": "Nog geen routepunten",
|
||||
"not-a-valid-email-address": "Het e-mailadres is ongeldig",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-a-valid-url": "Geen geldige URL",
|
||||
"not-completed": "Niet voltooid",
|
||||
"notification-list-create": "{user} created a new list",
|
||||
"notification-list-share": "{user} shared a list with you",
|
||||
"notification-new-follower": "You have a new follower",
|
||||
"notification-trail-comment": "{user} left a comment on your trail \"{trail}\"",
|
||||
"notification-trail-create": "{user} created a new trail",
|
||||
"notification-trail-share": "{user} shared a trail with you",
|
||||
"notifications": "Notifications",
|
||||
"off": "Off",
|
||||
"only-me": "Only me",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} heeft een nieuwe lijst gecreëerd",
|
||||
"notification-list-share": "{user} heeft een lijst gedeeld met jou",
|
||||
"notification-new-follower": "Je hebt een nieuwe volger",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} heeft een reactie achtergelaten op je wandelroute \"{trail}\"",
|
||||
"notification-trail-create": "{user} creëerde en nieuwe wandelroute",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} deelde een wandelroute met jou",
|
||||
"notifications": "Meldingen",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Uit",
|
||||
"only-me": "Alleen ik",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "of",
|
||||
"orientation": "Oriëntatie",
|
||||
"paper-size": "Papierformaat",
|
||||
"paragraph": "",
|
||||
"password": "Wachtwoord",
|
||||
"password-confirm": "Confirm password",
|
||||
"password-reset-sent": "A password reset email has been sent",
|
||||
"password-reset-text": "We will send a reset link to your email address.",
|
||||
"password-updated": "Password updated",
|
||||
"passwords-must-match": "Passwords must match",
|
||||
"password-confirm": "Bevestig wachtwoord",
|
||||
"password-reset-sent": "Er is een e-mail verzonden om uw wachtwoord opnieuw in te stellen",
|
||||
"password-reset-text": "Wij sturen een reset link naar uw e-mailadres.",
|
||||
"password-updated": "Wachtwoord bijgewerkt",
|
||||
"passwords-must-match": "Wachtwoorden moeten overeenkomen",
|
||||
"photos": "Foto's",
|
||||
"pick-a-trail": "Kies een wandelroute",
|
||||
"planned-a-trail": "planned a trail",
|
||||
"planned-tours": "Planned tours",
|
||||
"pick-a-trail": "Kies een Route",
|
||||
"planned-a-trail": "een route gepland",
|
||||
"planned-tours": "Geplande tochten",
|
||||
"polish": "Pools",
|
||||
"portuguese": "Portugees",
|
||||
"print": "Afdrukken",
|
||||
"privacy": "Privacy",
|
||||
"private": "Private",
|
||||
"private": "Privaat",
|
||||
"profile": "Profiel",
|
||||
"public": "Openbaar",
|
||||
"radius": "Straal",
|
||||
"read-more": "Read more",
|
||||
"read-more": "Lees meer",
|
||||
"register": "Registreren",
|
||||
"removed-trail-from": "De wandelroute is verwijderd van",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Route verwijderd van",
|
||||
"required": "Verplicht",
|
||||
"reset-password": "Reset Password",
|
||||
"road": "Road",
|
||||
"route": "{n, plural, =1 {Route} other {Routes}}",
|
||||
"route-point": "Route Point",
|
||||
"reset-password": "Wachtwoord opnieuw instellen",
|
||||
"road": "Weg",
|
||||
"route": "{n, plural,=1 {Tocht} other {Tochten}}",
|
||||
"route-point": "Routepunt",
|
||||
"save": "Bewaren",
|
||||
"save-list": "Save List",
|
||||
"save-trail": "Wandelroute bewaren",
|
||||
"save-your-trail-first": "Bewaar eerst je wandelroute",
|
||||
"save-list": "Bewaar lijst",
|
||||
"save-trail": "Bewaar Route",
|
||||
"save-your-trail-first": "Bewaar je route eerst",
|
||||
"search-cities": "Zoeken naar steden",
|
||||
"search-for-trails-places": "Zoeken naar locaties en wandelroutes",
|
||||
"search-places": "Search places",
|
||||
"search-trails": "Zoeken naar wandelroutes",
|
||||
"search-for-trails-places": "Zoek naar routes, lijsten en locaties",
|
||||
"search-places": "Zoek plaatsen",
|
||||
"search-trails": "Zoek routes",
|
||||
"select-list": "Kies een lijst",
|
||||
"settings": "Instellingen",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
"settings-notification-list-share": "Someone shared a list with you",
|
||||
"settings-notification-new-follower": "You have a new follower",
|
||||
"settings-notification-trail-comment": "Someone left a comment on your trail",
|
||||
"settings-notification-trail-create": "A user who you follow has created a trail",
|
||||
"settings-notification-trail-share": "Someone shared a trail with you",
|
||||
"settings-privacy-account-private": "Only you can see your profile. You will not appear in search results. Other users cannot follow you or share trails with you. You can still publish trails or lists.",
|
||||
"settings-privacy-account-public": "Everyone can see your profile. You appear in search results. Other users can follow you and share trails with you.",
|
||||
"settings-privacy-lists-private": "Your lists are private by default. No one except you will be able to see them. You can change this setting at any point for individual lists.",
|
||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||
"settings-saved": "Settings saved",
|
||||
"share": "Share",
|
||||
"share-profile": "Share profile",
|
||||
"share-this-list": "Share this list",
|
||||
"share-this-trail": "Share this trail",
|
||||
"shared": "Shared",
|
||||
"shared-by": "Shared by",
|
||||
"shared-with": "Shared with",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Een gebruiker die je volgt, heeft een lijst gecreëerd",
|
||||
"settings-notification-list-share": "Iemand heeft een lijst met je gedeeld",
|
||||
"settings-notification-new-follower": "Je hebt een nieuwe volger",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Iemand heeft een opmerking geplaatst bij je route",
|
||||
"settings-notification-trail-create": "Een gebruiker die je volgt, heeft een route gecreëerd",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Iemand heeft een route met je gedeeld",
|
||||
"settings-privacy-account-private": "Alleen jij kunt je profiel zien. Je verschijnt niet in de zoekresultaten. Andere gebruikers kunnen je niet volgen of routes met je delen. Je kunt nog steeds routes of lijsten publiceren.",
|
||||
"settings-privacy-account-public": "Iedereen kan je profiel zien. Je verschijnt in de zoekresultaten. Andere gebruikers kunnen je volgen en ervaringen met je delen.",
|
||||
"settings-privacy-lists-private": "Je lijsten zijn standaard privé. Niemand behalve jij kan ze zien. Je kunt deze instelling op elk moment voor individuele lijsten wijzigen.",
|
||||
"settings-privacy-lists-public": "Je lijsten zijn standaard openbaar. Iedereen kan ze zien. Je kunt deze instelling op elk moment voor individuele lijsten wijzigen.",
|
||||
"settings-privacy-trails-private": "Je routes zijn standaard privé. Niemand behalve jij kan ze zien. Je kunt deze instelling op elk moment voor individuele routes wijzigen.",
|
||||
"settings-privacy-trails-public": "Je routes zijn standaard openbaar. Iedereen kan ze zien. Je kunt deze instelling op elk moment voor individuele routes wijzigen.",
|
||||
"settings-saved": "Instellingen bewaard",
|
||||
"share": "Deel",
|
||||
"share-profile": "Deel profiel",
|
||||
"share-this-list": "Deel deze lijst",
|
||||
"share-this-trail": "Deel deze route",
|
||||
"shared": "Gedeeld",
|
||||
"shared-by": "Gedeeld door",
|
||||
"shared-with": "Gedeeld met",
|
||||
"shortest": "",
|
||||
"show-in-overview": "Tonen op overzicht",
|
||||
"show-on-map": "Tonen op kaart",
|
||||
"slogan": "Bewaar je avonturen!",
|
||||
"slope": "Slope",
|
||||
"someone": "Someone",
|
||||
"slope": "helling",
|
||||
"someone": "Iemand",
|
||||
"sort": "Sorteren",
|
||||
"spanish": "Spaans",
|
||||
"speed": "Speed",
|
||||
"speed": "Snelheid",
|
||||
"start": "Start",
|
||||
"statistics": "Statistics",
|
||||
"stop-drawing": "Stop drawing",
|
||||
"stop-editing": "Stop editing",
|
||||
"statistics": "Statistieken",
|
||||
"stop-drawing": "Stop met tekenen",
|
||||
"stop-editing": "Stop met bewerken",
|
||||
"summit-book": "Bergtopboek",
|
||||
"table": "Tabel",
|
||||
"tags": "Tags",
|
||||
"tags": "Labels",
|
||||
"text": "Tekst",
|
||||
"tilesets": "Custom tilesets",
|
||||
"top-speed": "Top Speed",
|
||||
"trail": "{n, plural, =1 {Wandelroute} other {Wandelroutes}}",
|
||||
"trail-not-shared": "Not shared with anyone",
|
||||
"trail-saved-successfully": "Trail saved successfully",
|
||||
"trails-for-you": "Wandelroutes voor jou",
|
||||
"unchanged": "unchanged",
|
||||
"tilesets": "Aangepaste tegelsets",
|
||||
"top-speed": "Topsnelheid",
|
||||
"trail": "{n, plural, =1 {Route} other {Routes}}",
|
||||
"trail-not-shared": "Niet gedeeld met iemand",
|
||||
"trail-saved-successfully": "Route succesvol bewaard",
|
||||
"trails-for-you": "Routes voor jou",
|
||||
"unchanged": "Ongewijzigd",
|
||||
"units": "Eenheden",
|
||||
"unlink": "",
|
||||
"upload-file": "Bestand uploaden",
|
||||
"upload-gpx": "GPX-bestand uploaden",
|
||||
"upload-new-file": "Upload new file",
|
||||
"uploaded": "uploaded",
|
||||
"upload-new-file": "Nieuw bestand uploaden",
|
||||
"uploaded": "geüpload",
|
||||
"use-hills": "",
|
||||
"use-roads": "Use Roads",
|
||||
"use-roads": "Gebruik wegen",
|
||||
"username": "Gebruikersnaam",
|
||||
"view": "View",
|
||||
"view": "Weergave",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
|
||||
"waypoints": "{n, plural,=1 {Routepunt} other {Routepunten}}",
|
||||
"welcome_to": "Welkom bij",
|
||||
"width": "Width",
|
||||
"width": "Breedte",
|
||||
"wrong-username-or-password": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"you-can": "You can"
|
||||
"you-can": "Jij kunt"
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Usuń",
|
||||
"delete-account": "Usuń Konto",
|
||||
"delete-list-confirm": "Czy na pewno chcesz usunąć tę listę? Ścieżki na liście zostaną zachowane.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Czy na pewno chcesz usunąć tą Ścieżkę? Ta akcja jest nieodwracalna.",
|
||||
"describe-your-trail": "Opisz swoją ścieżkę",
|
||||
"description": "Opis",
|
||||
@@ -101,7 +102,7 @@
|
||||
"empty-activities": "{username} nie ma jeszcze aktywności",
|
||||
"empty-bio": "{username} jeszcze nie dodał biogramu",
|
||||
"empty-lists": "{username} nie ma publicznych list",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Angielski",
|
||||
"entry": "Pozycja",
|
||||
"error-creating-user": "Błąd tworzenia użytkownika",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Błąd podczas logowania",
|
||||
"error-during-password-reset": "Nie udało się wysłać e-maila z resetowaniem hasła",
|
||||
"error-exporting-trail": "Błąd podczas eksportowania szlaku",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Błąd zapisu do komoot",
|
||||
"error-posting-comment": "Błąd wysyłania komentarza",
|
||||
"error-printing-map": "Błąd podczas drukowania mapy",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Skoncentruj mapę na",
|
||||
"follow": "Obserwuj",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Obserwujący",
|
||||
"following": "Obserwowane",
|
||||
"forgot-your-password": "Zapomniałeś hasła?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "Niemiecki",
|
||||
"get-position-from-exif": "Odczytaj współrzędne z danych EXIF",
|
||||
"grid": "Siatka",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "Pomoc",
|
||||
"hero_section_0_text": "Eksploruj ekscytujące szlaki, zapisz swoje ulubione, doświadcz piękna naturalnego świata. Znajdź swoją następną przygodę!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "Importuj",
|
||||
"import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...",
|
||||
"include-description": "Dołącz opis",
|
||||
"include-waypoints": "Uwzględnij punkty trasy",
|
||||
"integration-description-komoot": "Synchronizuje trasy kamoot z wanderer w równych odstępach.",
|
||||
"integration-description-strava": "Synchronizuje trasy i aktywność z wanderer w równych odstępach.",
|
||||
"integration-disabled": "integracja wyłączona",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Język",
|
||||
"latitude": "Szerokość",
|
||||
"license": "Licencja",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link skopiowany!",
|
||||
"list": "{n, plural, =1 {Lista} other {Listy}}",
|
||||
"list-not-shared": "Nikomu nie udostępniony",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Nieprawidłowy adres email",
|
||||
"not-a-valid-url": "Wadliwy URL",
|
||||
"not-completed": "Nie dokończono",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} utworzył(a) nową listę",
|
||||
"notification-list-share": "{user} udostępnił(a) tobie listę",
|
||||
"notification-new-follower": "Masz nowego obserwującego",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} skomentował(a) twój szlak \"{trail}\"",
|
||||
"notification-trail-create": "{user} utworzył nowy szlak",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} udostępnił(a) tobie szlak",
|
||||
"notifications": "Powiadomienia",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Brak",
|
||||
"only-me": "Tylko ja",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "lub",
|
||||
"orientation": "Orientacja",
|
||||
"paper-size": "Rozmiar papieru",
|
||||
"paragraph": "",
|
||||
"password": "Hasło",
|
||||
"password-confirm": "Potwierdź hasło",
|
||||
"password-reset-sent": "E-mail z resetowaniem hasła został wysłany",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Promień",
|
||||
"read-more": "Czytaj dalej",
|
||||
"register": "Zarejestruj",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Usunięto szlak z",
|
||||
"required": "Wymagane",
|
||||
"reset-password": "Resetuj hasło",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Szukaj szlaków",
|
||||
"select-list": "Wybierz Listę",
|
||||
"settings": "Ustawienia",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Użytkownik, którego obserwujesz, utworzył listę",
|
||||
"settings-notification-list-share": "Ktoś udostępnił tobie listę",
|
||||
"settings-notification-new-follower": "Masz nowego obserwującego",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Ktoś skomentował twój szlak",
|
||||
"settings-notification-trail-create": "Użytkownik, którego obserwujesz, utworzył szlak",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Ktoś udostępnił tobie szlak",
|
||||
"settings-privacy-account-private": "Tylko ty możesz zobaczyć swój profil. Nie będziesz się pojawiać w wynikach wyszukiwania. Inni użytkownicy nie mogą cię obserwować ani udostępniać ci szlaków. Wciąż możesz publikować szlaki i listy.",
|
||||
"settings-privacy-account-public": "Wszyscy mogą zobaczyć twój profil. Będziesz się pojawiać w wynikach wyszukiwania. Inni użytkownicy mogą cię obserwować i udostępniać ci szlaki.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "Szlaki dla ciebie",
|
||||
"unchanged": "bez zmian",
|
||||
"units": "Jednostki",
|
||||
"unlink": "",
|
||||
"upload-file": "Przesyłanie pliku",
|
||||
"upload-gpx": "Importuj GPX",
|
||||
"upload-new-file": "Prześlij nowy plik",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "Nazwa Użytkownika",
|
||||
"view": "Widok",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, one {Punkt} few {Punkty} many {Punktów}=1 {Punkt} other {Punktów}}",
|
||||
"welcome_to": "Witaj w",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "Excluir",
|
||||
"delete-account": "Excluir conta",
|
||||
"delete-list-confirm": "Você quer mesmo excluir esta lista? As trilhas na lista ainda estarão disponíveis.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Você realmente quer excluir esta trilha? Esta ação não pode ser desfeita.",
|
||||
"describe-your-trail": "Descreva sua trilha",
|
||||
"description": "Descrição",
|
||||
@@ -101,7 +102,7 @@
|
||||
"empty-activities": "{username} has no activity yet",
|
||||
"empty-bio": "{username} has not added a bio yet",
|
||||
"empty-lists": "{username} has no public lists",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Inglês",
|
||||
"entry": "Entrada",
|
||||
"error-creating-user": "Erro ao criar utilizador",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "Erro durante o ‘login’",
|
||||
"error-during-password-reset": "Unable to send password reset email",
|
||||
"error-exporting-trail": "Erro na exportação do percurso",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-posting-comment": "Error posting comment",
|
||||
"error-printing-map": "Erro na impressão do mapa",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "Centrar mapa em",
|
||||
"follow": "Follow",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"forgot-your-password": "Forgot your password?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "Alemão",
|
||||
"get-position-from-exif": "Obter coordenadas dos dados EXIF",
|
||||
"grid": "Grelha",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "Ajuda",
|
||||
"hero_section_0_text": "Explore trilhas emocionantes, salve seus favoritos e experimente a beleza da natureza. Encontre sua próxima aventura!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "Importar",
|
||||
"import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...",
|
||||
"include-description": "Incluir descrição",
|
||||
"include-waypoints": "Incluir pontos de passagem",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "Língua",
|
||||
"latitude": "Latitude",
|
||||
"license": "Licença",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Link copiado!",
|
||||
"list": "{n, plural, =1 {Lista} other {Listas}}",
|
||||
"list-not-shared": "Não partilhado com ninguém",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "Não um endereço de e-mail válido",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-completed": "Não preenchido",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} created a new list",
|
||||
"notification-list-share": "{user} shared a list with you",
|
||||
"notification-new-follower": "You have a new follower",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} left a comment on your trail \"{trail}\"",
|
||||
"notification-trail-create": "{user} created a new trail",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} shared a trail with you",
|
||||
"notifications": "Notifications",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Desligar",
|
||||
"only-me": "Only me",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "ou",
|
||||
"orientation": "Orientação",
|
||||
"paper-size": "Tamanho do papel",
|
||||
"paragraph": "",
|
||||
"password": "Senha",
|
||||
"password-confirm": "Confirm password",
|
||||
"password-reset-sent": "A password reset email has been sent",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "Raio",
|
||||
"read-more": "Ler mais",
|
||||
"register": "Registo",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Trilha removida de",
|
||||
"required": "Obrigatório",
|
||||
"reset-password": "Reset Password",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "Procurar trilhos",
|
||||
"select-list": "Selecionar lista",
|
||||
"settings": "Definições",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
"settings-notification-list-share": "Someone shared a list with you",
|
||||
"settings-notification-new-follower": "You have a new follower",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Someone left a comment on your trail",
|
||||
"settings-notification-trail-create": "A user who you follow has created a trail",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Someone shared a trail with you",
|
||||
"settings-privacy-account-private": "Only you can see your profile. You will not appear in search results. Other users cannot follow you or share trails with you. You can still publish trails or lists.",
|
||||
"settings-privacy-account-public": "Everyone can see your profile. You appear in search results. Other users can follow you and share trails with you.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "Trilhos para si",
|
||||
"unchanged": "unchanged",
|
||||
"units": "Unidades",
|
||||
"unlink": "",
|
||||
"upload-file": "Subir arquivo",
|
||||
"upload-gpx": "Carregar GPX",
|
||||
"upload-new-file": "Upload new file",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "Nome de utilizador",
|
||||
"view": "Ver",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Ponto de passagem} other {Pontos de passagem}}",
|
||||
"welcome_to": "Bem-vindo ao",
|
||||
|
||||
357
web/src/lib/i18n/locales/ru.json
Normal file
357
web/src/lib/i18n/locales/ru.json
Normal file
@@ -0,0 +1,357 @@
|
||||
{
|
||||
"Biking": "Велоспорт",
|
||||
"Canoeing": "Каякинг",
|
||||
"Climbing": "Скалолазание",
|
||||
"Hiking": "Пеший туризм",
|
||||
"Skiing": "Лыжный спорт",
|
||||
"Walking": "Прогулка",
|
||||
"about": "О",
|
||||
"account-delete-confirm": "Вы собираетесь удалить аккаунт. Все ваши треки также будут удалены. Продолжить?",
|
||||
"account-privacy": "Приватность аккаунта",
|
||||
"activity": "{n, plural, =1 {Активность} other {Активностей}}",
|
||||
"add-bio": "Рассказать о себе",
|
||||
"add-entry": "Добавить запись",
|
||||
"add-to-list": "Добавить в список",
|
||||
"add-waypoint": "Добавить путевую точку",
|
||||
"added-trail-to": "Трек добавлен в",
|
||||
"after": "После",
|
||||
"all-activities": "Все активности",
|
||||
"alphabetical": "По алфавиту",
|
||||
"already-account": "Уже есть аккаунт?",
|
||||
"altitude": "Высота",
|
||||
"api-documentation": "Документация API",
|
||||
"author": "Автор",
|
||||
"avatar": "Аватар",
|
||||
"average-speed": "Ср. скорость",
|
||||
"avoid-bad-surfaces": "Избегать неровные дороги",
|
||||
"back": "Назад",
|
||||
"back-to-login": "Вернуться ко входу",
|
||||
"basic-info": "Основная информация",
|
||||
"before": "До",
|
||||
"bike-type": "Тип велосипеда",
|
||||
"by": "От",
|
||||
"can": "можно",
|
||||
"cancel": "Отмена",
|
||||
"car": "Автомобиль:",
|
||||
"card": "{n, plural, =1 {Карточка} other {Карточки}}",
|
||||
"categories": "Категории",
|
||||
"category": "Категория",
|
||||
"change": "Изменить",
|
||||
"change-email": "Изменить email",
|
||||
"change-password": "Изменить пароль",
|
||||
"changelog": "История изменений",
|
||||
"chinese": "Китайский (упрощ.)",
|
||||
"clear-all": "Очистить всё",
|
||||
"close": "Закрыть",
|
||||
"comment": "{n, plural, =1 {Комментарий} other {Комментарии}}",
|
||||
"completed": "Завершено",
|
||||
"completed-a-trail": "завершил(ла) трек",
|
||||
"completed-tours": "Пройденные маршруты",
|
||||
"completion-status": "Статус прохождения",
|
||||
"confirm": "Подтвердить",
|
||||
"confirm-deletion": "Подтвердите удаление",
|
||||
"confirm-publish": "Подтвердить публикацию",
|
||||
"confirm-share": "Подтвердить общий доступ",
|
||||
"connect": "Подключить",
|
||||
"contribute": "Помощь в разработке",
|
||||
"copy-link": "Копировать ссылку",
|
||||
"create-new-list": "Создать новый список",
|
||||
"create-waypoint": "Создать путевую точку",
|
||||
"creation-date": "Дата создания",
|
||||
"cross": "Циклокросс",
|
||||
"current-password": "Текущий пароль",
|
||||
"cycling": "Велосипед",
|
||||
"cycling-speed": "Скорость велосипеда",
|
||||
"danger-zone": "Внимание!",
|
||||
"date": "Дата",
|
||||
"default-category": "Категория по умолчанию",
|
||||
"default-location": "Место по умолчанию",
|
||||
"degrees": "Градусы",
|
||||
"delete": "Удалить",
|
||||
"delete-account": "Удалить аккаунт",
|
||||
"delete-list-confirm": "Удалить этот список? Треки в нём останутся доступны.",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "Удалить этот трек? Действие необратимо.",
|
||||
"describe-your-trail": "Описание трека",
|
||||
"description": "Описание",
|
||||
"difficult": "Сложный",
|
||||
"difficulty": "Сложность",
|
||||
"directions": "Как добраться",
|
||||
"display": "Отображение",
|
||||
"display-as": "Отображать как",
|
||||
"distance": "Расстояние",
|
||||
"documentation": "Документация",
|
||||
"download": "Скачать",
|
||||
"draw-a-route": "Нарисовать маршрут",
|
||||
"driving": "На машине",
|
||||
"duplicate": "Дублировать",
|
||||
"duration": "Длительность",
|
||||
"dutch": "Голландский",
|
||||
"easy": "Лёгкий",
|
||||
"edit": "Редактировать",
|
||||
"edit-entry": "Редактировать запись",
|
||||
"edit-list": "Редактировать список",
|
||||
"edit-route": "Редактировать маршрут",
|
||||
"edit-waypoint": "Редактировать путевую точку",
|
||||
"edited": "изменено",
|
||||
"elevation-gain": "Набор высоты",
|
||||
"elevation-loss": "Сброс высоты",
|
||||
"email": "Email",
|
||||
"email-updated": "Email обновлён",
|
||||
"email-verified": "Email подтверждён",
|
||||
"empty-activities": "у {username} ещё нет активностей",
|
||||
"empty-bio": "{username} ещё не рассказал(а) о себе",
|
||||
"empty-lists": "у {username} нет публичных списков",
|
||||
"enable-auto-routing": "Авто-маршрутизация",
|
||||
"english": "Английский",
|
||||
"entry": "Запись",
|
||||
"error-creating-user": "Ошибка создания пользователя",
|
||||
"error-disabling-strava-integration": "Ошибка отключения Strava",
|
||||
"error-during-login": "Ошибка входа",
|
||||
"error-during-password-reset": "Не удалось отправить email сброса пароля",
|
||||
"error-exporting-trail": "Ошибка экспорта трека",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Ошибка входа в Komoot",
|
||||
"error-posting-comment": "Ошибка отправки комментария",
|
||||
"error-printing-map": "Ошибка печати карты",
|
||||
"error-reading-file": "Ошибка чтения файла",
|
||||
"error-saving-list": "Ошибка сохранения списка",
|
||||
"error-saving-trail": "Ошибка сохранения трека",
|
||||
"error-setting-up-integration": "Ошибка настройки {provider}",
|
||||
"error-updating-password": "Ошибка изменения пароля",
|
||||
"error-updating-strava-integration": "Ошибка обновления Strava",
|
||||
"est-duration": "Продолжительность",
|
||||
"explore": "Изучить",
|
||||
"explore-some-trails": "Изучите треки",
|
||||
"export": "Экспорт",
|
||||
"export-all-trails": "Экспорт всех треков",
|
||||
"favourite-sport": "Любимый вид спорта",
|
||||
"features": "Особенности",
|
||||
"file-format": "Формат файла",
|
||||
"file-too-big": "Файл {file} слишком большой (max. {size})",
|
||||
"filter-categories": "Фильтр категорий",
|
||||
"filter-difficulty": "Фильтр сложности",
|
||||
"filter-tags": "Фильтр меток",
|
||||
"finish": "Финиш",
|
||||
"fixed-speed": "Крейсерская скорость",
|
||||
"focus-map-on": "Фокусироваться на",
|
||||
"follow": "Подписаться",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Подписчики",
|
||||
"following": "Подписки",
|
||||
"forgot-your-password": "Забыли пароль?",
|
||||
"french": "Французский",
|
||||
"from-file": "Из файла",
|
||||
"from-photos": "Из фотографии",
|
||||
"from-url": "По ссылке",
|
||||
"german": "Немецкий",
|
||||
"get-position-from-exif": "Координаты из EXIF",
|
||||
"grid": "Сетка",
|
||||
"heading": "",
|
||||
"height": "Высота",
|
||||
"help": "Помощь",
|
||||
"hero_section_0_text": "Исследуйте треки, сохраняйте избранное и наслаждайтесь природой. Найдите новое приключение!",
|
||||
"hero_section_1_heading": "Похоже, здесь пока нет треков.",
|
||||
"hero_section_1_text": "Маршруты, которые могут вам понравиться. Или посмотрите полный список.",
|
||||
"hero_section_1_text_alternative": "Начните с сохранения вашего последнего приключения.",
|
||||
"hero_section_2_text": "Знаете ли вы, что можно сохранять не только пешие походы. Есть множество и других категорий для любых ваших активностей.",
|
||||
"hiking": "Пеший туризм",
|
||||
"home": "Главная",
|
||||
"hungarian": "Венгерский",
|
||||
"hybrid": "Гибрид",
|
||||
"icon": "Иконка",
|
||||
"imperial": "Имперская",
|
||||
"import": "Импорт",
|
||||
"import-hint": "Перетащите GPX, FIT, KML или TCX файлы сюда...",
|
||||
"include-description": "Добавить описание",
|
||||
"integration-description-komoot": "Синхронизирует ваши данные с Komoot.",
|
||||
"integration-description-strava": "Синхронизирует ваши данные со Strava.",
|
||||
"integration-disabled": "интеграция отключена",
|
||||
"integration-enabled": "интеграция включена",
|
||||
"integrations": "Интеграции",
|
||||
"invalid-date": "Неверная дата",
|
||||
"invalid-username": "Некорректное имя пользователя",
|
||||
"italian": "Итальянский",
|
||||
"joined": "Зарегистрирован",
|
||||
"language": "Язык",
|
||||
"latitude": "Широта",
|
||||
"license": "Лицензия",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "Ссылка скопирована!",
|
||||
"list": "{n, plural, =1 {Список} other {Списки}}",
|
||||
"list-not-shared": "Нет общего доступа",
|
||||
"list-public-warning": "Все треки в списке станут публичными.",
|
||||
"list-saved-successfully": "Список сохранён",
|
||||
"list-share-warning": "Общий доступ к списку открывает доступ ко всем трекам в нём.",
|
||||
"list-share-warning-update": "Новые треки будут доступны всем, у кого есть доступ к списку.",
|
||||
"location": "Местоположение",
|
||||
"login": "Войти",
|
||||
"login-details": "Данные для входа",
|
||||
"logout": "Выйти",
|
||||
"longitude": "Долгота",
|
||||
"loop": "Круг",
|
||||
"make-one": "Создайте!",
|
||||
"make-thumbnail": "Сделать миниатюру",
|
||||
"map": "Карта",
|
||||
"max-hiking-difficulty": "Макс. сложность",
|
||||
"metric": "Метрическая",
|
||||
"moderate": "Средний",
|
||||
"mountain": "Горный",
|
||||
"must-be-at-least-n-characters-long": "Минимум {n} символов",
|
||||
"must-be-at-most-n-characters-long": "Максимум {n} символов",
|
||||
"my-account": "Мой аккаунт",
|
||||
"n-days-ago": "{n} дней назад",
|
||||
"n-hours-ago": "{n} часов назад",
|
||||
"n-minutes-ago": "{n} минут назад",
|
||||
"n-months-ago": "{n} месяцев назад",
|
||||
"n-seconds-ago": "{n, plural, =0 {только что} other {{n} секунд назад}}",
|
||||
"n-years-ago": "{n} лет назад",
|
||||
"name": "Название",
|
||||
"near": "Рядом",
|
||||
"new-list": "Новый список",
|
||||
"new-password": "Новый пароль",
|
||||
"new-password-error": "Ошибка обновления пароля",
|
||||
"new-password-success": "Пароль успешно изменен",
|
||||
"new-password-text": "Установить новый пароль",
|
||||
"new-trail": "Новый трек",
|
||||
"no-account": "Нет аккаунта?",
|
||||
"no-comments-so-far": "Пока нет комментариев",
|
||||
"no-data": "Нет данных",
|
||||
"no-description-for-now": "Пока нет описания",
|
||||
"no-gps-data-in-image": "",
|
||||
"no-grid": "Без сетки",
|
||||
"no-notifications": "Нет уведомлений",
|
||||
"no-photos-here": "Здесь нет фото/видео",
|
||||
"no-preference": "Любые",
|
||||
"no-results": "Ничего не найдено",
|
||||
"no-routes-added": "Нет добавленных маршрутов",
|
||||
"no-waypoints-yet": "Нет путевых точек",
|
||||
"not-a-valid-email-address": "Некорректный email",
|
||||
"not-a-valid-url": "Некорректный URL",
|
||||
"not-completed": "Не завершено",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} создал(а) новый список",
|
||||
"notification-list-share": "{user} предоставил(а) доступ к списку",
|
||||
"notification-new-follower": "У вас новый подписчик",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} прокомментировал(а) ваш трек \"{trail}\"",
|
||||
"notification-trail-create": "{user} создал(а) новый трек",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} предоставил(а) доступ к треку",
|
||||
"notifications": "Уведомления",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "Выкл",
|
||||
"only-me": "Только я",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "или",
|
||||
"orientation": "Ориентация",
|
||||
"paper-size": "Размер бумаги",
|
||||
"paragraph": "",
|
||||
"password": "Пароль",
|
||||
"password-confirm": "Подтвердите пароль",
|
||||
"password-reset-sent": "Письмо для сброса пароля отправлено",
|
||||
"password-reset-text": "Мы отправим ссылку для сброса на ваш email.",
|
||||
"password-updated": "Пароль обновлён",
|
||||
"passwords-must-match": "Пароли не совпадают",
|
||||
"photos": "Фото и Видео",
|
||||
"pick-a-trail": "Выберите трек",
|
||||
"planned-a-trail": "запланировал(а) трек",
|
||||
"planned-tours": "Запланированные маршруты",
|
||||
"polish": "Польский",
|
||||
"portuguese": "Португальский",
|
||||
"print": "Печать",
|
||||
"privacy": "Приватность",
|
||||
"private": "Зкрытый",
|
||||
"profile": "Профиль",
|
||||
"public": "Открытый",
|
||||
"radius": "Радиус",
|
||||
"read-more": "Подробнее",
|
||||
"register": "Регистрация",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Трек удалён из",
|
||||
"required": "Обязательно",
|
||||
"reset-password": "Сбросить пароль",
|
||||
"road": "Шоссе",
|
||||
"route": "{n, plural, =1 {Маршрут} other {Маршрутов}}",
|
||||
"route-point": "Точка маршрута",
|
||||
"save": "Сохранить",
|
||||
"save-list": "Сохранить список",
|
||||
"save-trail": "Сохранить трек",
|
||||
"save-your-trail-first": "Сначала сохраните трек",
|
||||
"search-cities": "Поиск по городам",
|
||||
"search-for-trails-places": "Поиск треков, списков, мест",
|
||||
"search-places": "Поиск мест",
|
||||
"search-trails": "Поиск треков",
|
||||
"select-list": "Выбрать список",
|
||||
"settings": "Настройки",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Пользователь, на которого вы подписаны, создал список",
|
||||
"settings-notification-list-share": "Вам предоставили доступ к списку",
|
||||
"settings-notification-new-follower": "У вас новый подписчик",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Кто-то прокомментировал ваш трек",
|
||||
"settings-notification-trail-create": "Пользователь, на которого вы подписаны, создал трек",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Вам предоставили доступ к треку",
|
||||
"settings-privacy-account-private": "Только вы видите свой профиль. Не отображается при поиске. Другие пользователи не могут подписаться на вас или поделиться треками. Вы по-прежнему можете публиковать маршруты или списки.",
|
||||
"settings-privacy-account-public": "Все видят ваш профиль. Отображается при поиске. Другие пользователи могут подписаться на вас и поделиться треками.",
|
||||
"settings-privacy-lists-private": "Списки по умолчанию закрытые. Их видите только вы. Настройку можно изменить для каждого списка.",
|
||||
"settings-privacy-lists-public": "Списки по умолчанию открытые. Их видят все. Настройку можно изменить для каждого списка.",
|
||||
"settings-privacy-trails-private": "Треки по умолчанию закрытые. Их видите только вы. Настройку можно изменить для каждого трека.",
|
||||
"settings-privacy-trails-public": "Треки по умолчанию открытые. Их видят все. Настройку можно изменить для каждого трека.",
|
||||
"settings-saved": "Настройки сохранены",
|
||||
"share": "Поделиться",
|
||||
"share-profile": "Поделиться профилем",
|
||||
"share-this-list": "Открыть доступ к списку",
|
||||
"share-this-trail": "Открыть доступ к треку",
|
||||
"shared": "Общий доступ",
|
||||
"shared-by": "Доступ от",
|
||||
"shared-with": "Доступно для",
|
||||
"shortest": "",
|
||||
"show-in-overview": "Быстрый просмотр",
|
||||
"show-on-map": "Показать на карте",
|
||||
"slogan": "Сохраняйте ваши приключения!",
|
||||
"slope": "Уклон",
|
||||
"someone": "Кто-то",
|
||||
"sort": "Сортировка",
|
||||
"spanish": "Испанский",
|
||||
"speed": "Скорость",
|
||||
"start": "Старт",
|
||||
"statistics": "Статистика",
|
||||
"stop-drawing": "Закончить рисование",
|
||||
"stop-editing": "Закончить редактирование",
|
||||
"summit-book": "История поездок",
|
||||
"table": "Таблица",
|
||||
"tags": "Метки",
|
||||
"text": "Текст",
|
||||
"tilesets": "Пользовательские тайлы",
|
||||
"top-speed": "Макс. скорость",
|
||||
"trail": "{n, plural, =1 {Трек} other {Треки}}",
|
||||
"trail-not-shared": "Нет общего доступа",
|
||||
"trail-saved-successfully": "Трек сохранён",
|
||||
"trails-for-you": "Треки для вас",
|
||||
"unchanged": "не изменено",
|
||||
"units": "Единицы измерения",
|
||||
"unlink": "",
|
||||
"upload-file": "Загрузить файл",
|
||||
"upload-gpx": "Загрузить GPX",
|
||||
"upload-new-file": "Загрузить новый файл",
|
||||
"uploaded": "загружено",
|
||||
"use-hills": "",
|
||||
"use-roads": "Использование дорог",
|
||||
"username": "Никнейм",
|
||||
"view": "Просмотр",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {Путевая точка} other {Путевые точки}}",
|
||||
"welcome_to": "Добро пожаловать в",
|
||||
"width": "Ширина",
|
||||
"wrong-username-or-password": "Неверный логин или пароль",
|
||||
"you-can": "Вы можете"
|
||||
}
|
||||
@@ -70,6 +70,7 @@
|
||||
"delete": "删除",
|
||||
"delete-account": "注销账户",
|
||||
"delete-list-confirm": "您确认要删除当前列表?列表中所有行程仍然继续保留不会被删除。",
|
||||
"delete-summit-log-confirm": "Do you really want to delete this summit log? This action cannot be undone.",
|
||||
"delete-trail-confirm": "您确认想要删除当前行程?此操作不可撤回。",
|
||||
"describe-your-trail": "行程描述",
|
||||
"description": "描述",
|
||||
@@ -101,7 +102,7 @@
|
||||
"empty-activities": "{username} has no activity yet",
|
||||
"empty-bio": "{username} has not added a bio yet",
|
||||
"empty-lists": "{username} has no public lists",
|
||||
"enable-auto-routing": "",
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "英语",
|
||||
"entry": "日程",
|
||||
"error-creating-user": "创建用户错误",
|
||||
@@ -109,6 +110,7 @@
|
||||
"error-during-login": "登录错误",
|
||||
"error-during-password-reset": "Unable to send password reset email",
|
||||
"error-exporting-trail": "导出路线失败",
|
||||
"error-liking-trail": "",
|
||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||
"error-posting-comment": "Error posting comment",
|
||||
"error-printing-map": "打印地图失败",
|
||||
@@ -134,6 +136,7 @@
|
||||
"fixed-speed": "Fixed Speed",
|
||||
"focus-map-on": "地图聚焦于",
|
||||
"follow": "Follow",
|
||||
"follow-request-pending": "Request pending",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"forgot-your-password": "忘记密码?",
|
||||
@@ -144,6 +147,7 @@
|
||||
"german": "德语",
|
||||
"get-position-from-exif": "从EXIF数据获取坐标",
|
||||
"grid": "网格",
|
||||
"heading": "",
|
||||
"height": "Height",
|
||||
"help": "帮助",
|
||||
"hero_section_0_text": "探索激动人心的探险,收藏最爱行程,体验美丽地大自然。找到你的下一次冒险!",
|
||||
@@ -160,6 +164,7 @@
|
||||
"import": "导入",
|
||||
"import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...",
|
||||
"include-description": "包含描述",
|
||||
"include-waypoints": "包括航点",
|
||||
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||
"integration-disabled": "integration disabled",
|
||||
@@ -172,6 +177,9 @@
|
||||
"language": "语言",
|
||||
"latitude": "维度",
|
||||
"license": "开源协议",
|
||||
"like-status": "Like Status",
|
||||
"liked": "Liked",
|
||||
"likes": "Likes",
|
||||
"link-copied": "链接已复制",
|
||||
"list": "{n, plural, =1 {列表} other {列表}}",
|
||||
"list-not-shared": "未与任何人分享",
|
||||
@@ -224,18 +232,26 @@
|
||||
"not-a-valid-email-address": "无效电子邮箱地址",
|
||||
"not-a-valid-url": "Not a valid URL",
|
||||
"not-completed": "未完成",
|
||||
"notification-comment-mention": "{user} mentioned you in a comment",
|
||||
"notification-list-create": "{user} created a new list",
|
||||
"notification-list-share": "{user} shared a list with you",
|
||||
"notification-new-follower": "You have a new follower",
|
||||
"notification-summit-log-create": "{user} created a summit log on your trail \"{trail}\"",
|
||||
"notification-summit-log-mention": "{user} mentioned you in a summit log",
|
||||
"notification-trail-comment": "{user} left a comment on your trail \"{trail}\"",
|
||||
"notification-trail-create": "{user} created a new trail",
|
||||
"notification-trail-like": "{user} liked your trail \"{trail}\"",
|
||||
"notification-trail-mention": "{user} mentioned you in a trail",
|
||||
"notification-trail-share": "{user} shared a trail with you",
|
||||
"notifications": "Notifications",
|
||||
"object-share-error": "A {object} must be public to be shared across instances.",
|
||||
"off": "关闭",
|
||||
"only-me": "Only me",
|
||||
"open-in-new-tab": "Open in new tab",
|
||||
"or": "或",
|
||||
"orientation": "方向",
|
||||
"paper-size": "纸张大小",
|
||||
"paragraph": "",
|
||||
"password": "密码",
|
||||
"password-confirm": "确认密码",
|
||||
"password-reset-sent": "A password reset email has been sent",
|
||||
@@ -256,6 +272,7 @@
|
||||
"radius": "半径",
|
||||
"read-more": "阅读更多",
|
||||
"register": "注册",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "路线已删除自",
|
||||
"required": "必填",
|
||||
"reset-password": "重置密码",
|
||||
@@ -272,11 +289,16 @@
|
||||
"search-trails": "搜索路线",
|
||||
"select-list": "选择列表",
|
||||
"settings": "设置",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
"settings-notification-list-share": "Someone shared a list with you",
|
||||
"settings-notification-new-follower": "You have a new follower",
|
||||
"settings-notification-summit-log-create": "Someone created a summit log on your trail",
|
||||
"settings-notification-summit-log-mention": "Someone mentioned you in a summit log",
|
||||
"settings-notification-trail-comment": "Someone left a comment on your trail",
|
||||
"settings-notification-trail-create": "A user who you follow has created a trail",
|
||||
"settings-notification-trail-like": "Somone liked your trail",
|
||||
"settings-notification-trail-mention": "Someone mentioned you in a trail",
|
||||
"settings-notification-trail-share": "Someone shared a trail with you",
|
||||
"settings-privacy-account-private": "Only you can see your profile. You will not appear in search results. Other users cannot follow you or share trails with you. You can still publish trails or lists.",
|
||||
"settings-privacy-account-public": "Everyone can see your profile. You appear in search results. Other users can follow you and share trails with you.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"trails-for-you": "推荐路线",
|
||||
"unchanged": "unchanged",
|
||||
"units": "单位",
|
||||
"unlink": "",
|
||||
"upload-file": "上传文件",
|
||||
"upload-gpx": "上传 GPX",
|
||||
"upload-new-file": "Upload new file",
|
||||
@@ -325,6 +348,7 @@
|
||||
"use-roads": "Use Roads",
|
||||
"username": "用户名",
|
||||
"view": "查看",
|
||||
"visibilty-status": "",
|
||||
"walking-speed": "",
|
||||
"waypoints": "{n, plural, =1 {路点} other {路点}}",
|
||||
"welcome_to": "欢迎",
|
||||
|
||||
14
web/src/lib/models/activitypub/activity.ts
Normal file
14
web/src/lib/models/activitypub/activity.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { User } from "../user";
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
iri: string;
|
||||
type: string;
|
||||
to: string[];
|
||||
cc: string[];
|
||||
bto: string[];
|
||||
bcc: string[];
|
||||
object: any;
|
||||
actor: string;
|
||||
published: string;
|
||||
}
|
||||
25
web/src/lib/models/activitypub/actor.ts
Normal file
25
web/src/lib/models/activitypub/actor.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { User } from "../user";
|
||||
|
||||
export interface Actor {
|
||||
id?: string;
|
||||
username: string;
|
||||
preferred_username: string;
|
||||
domain?: string;
|
||||
summary?: string;
|
||||
published?: string;
|
||||
followerCount?: number,
|
||||
followingCount?: number,
|
||||
iri: string;
|
||||
inbox: string;
|
||||
outbox?: string;
|
||||
icon?: string;
|
||||
followers?: string;
|
||||
following?: string;
|
||||
isLocal: boolean;
|
||||
public_key: string;
|
||||
last_fetched: string;
|
||||
user?: string
|
||||
expand?: {
|
||||
user?: User
|
||||
}
|
||||
}
|
||||
12
web/src/lib/models/activitypub/webfinger_response.ts
Normal file
12
web/src/lib/models/activitypub/webfinger_response.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface WebfingerResponse {
|
||||
subject: string
|
||||
aliases: string[]
|
||||
links: Link[]
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
rel: string
|
||||
type: string
|
||||
href: string
|
||||
}
|
||||
|
||||
@@ -3,15 +3,13 @@ import type { Comment } from "../comment";
|
||||
|
||||
const CommentCreateSchema = z.object({
|
||||
text: z.string(),
|
||||
rating: z.number().optional(),
|
||||
author: z.string().length(15),
|
||||
trail: z.string().length(15),
|
||||
trail: z.string(),
|
||||
|
||||
}) satisfies ZodType<Comment>
|
||||
|
||||
const CommentUpdateSchema = z.object({
|
||||
text: z.string().optional(),
|
||||
rating: z.number().optional(),
|
||||
}) satisfies ZodType<Partial<Comment>>
|
||||
|
||||
export { CommentCreateSchema, CommentUpdateSchema }
|
||||
@@ -2,10 +2,9 @@ import { z, ZodType } from "zod";
|
||||
import type { Follow } from "../follow";
|
||||
|
||||
const FollowCreateSchema = z.object({
|
||||
follower: z.string().length(15),
|
||||
followee: z.string().length(15),
|
||||
|
||||
}) satisfies ZodType<Follow>
|
||||
})
|
||||
|
||||
|
||||
export { FollowCreateSchema };
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z, ZodType } from "zod";
|
||||
import type { ListShare } from "../list_share";
|
||||
|
||||
const ListShareCreateSchema = z.object({
|
||||
user: z.string().length(15),
|
||||
actor: z.string().url(),
|
||||
list: z.string().length(15),
|
||||
permission: z.enum(["view", "edit"])
|
||||
|
||||
|
||||
@@ -12,11 +12,12 @@ const SummitLogCreateSchema = z.object({
|
||||
elevation_loss: z.number().nonnegative().optional(),
|
||||
duration: z.number().nonnegative().optional(),
|
||||
author: z.string().length(15),
|
||||
trail: z.string().length(15).optional(),
|
||||
photos: z.array(z.string()).default([])
|
||||
}) satisfies ZodType<Partial<SummitLog>>
|
||||
|
||||
const SummitLogUpdateSchema = z.object({
|
||||
date: z.string().date().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date").optional(),
|
||||
date: z.string().date().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date").optional(),
|
||||
text: z.string().optional(),
|
||||
gpx: z.string().optional(),
|
||||
distance: z.number().nonnegative().optional(),
|
||||
|
||||
10
web/src/lib/models/api/trail_like_schema.ts
Normal file
10
web/src/lib/models/api/trail_like_schema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z, ZodType } from "zod";
|
||||
import type { TrailLike } from "../trail_like";
|
||||
|
||||
const TrailLikeCreateSchema = z.object({
|
||||
actor: z.string().length(15),
|
||||
trail: z.string().length(15),
|
||||
|
||||
}) satisfies ZodType<TrailLike>
|
||||
|
||||
export { TrailLikeCreateSchema };
|
||||
@@ -19,7 +19,7 @@ const TrailCreateSchema = z.object({
|
||||
photos: z.array(z.string()).default([]),
|
||||
thumbnail: z.number().int().nonnegative().optional(),
|
||||
waypoints: z.array(z.string()).default([]),
|
||||
summit_logs: z.array(z.string()).default([]),
|
||||
like_count: z.number().int().min(0).optional().default(0),
|
||||
category: z.string().length(15).optional().or(z.literal('')),
|
||||
tags: z.array(z.string()).default([]),
|
||||
gpx: z.string().optional(),
|
||||
@@ -45,7 +45,7 @@ const TrailUpdateSchema = z.object({
|
||||
"photos+": z.string().optional(),
|
||||
thumbnail: z.number().int().nonnegative().optional(),
|
||||
waypoints: z.array(z.string()).optional(),
|
||||
summit_logs: z.array(z.string()).optional(),
|
||||
like_count: z.number().int().min(0).optional().default(0),
|
||||
category: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
gpx: z.string().optional(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z, ZodType } from "zod";
|
||||
import type { TrailShare } from "../trail_share";
|
||||
|
||||
const TrailShareCreateSchema = z.object({
|
||||
user: z.string().length(15),
|
||||
actor: z.string().url(),
|
||||
trail: z.string().length(15),
|
||||
permission: z.enum(["view", "edit"])
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { UserAnonymous } from "./user";
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
|
||||
export class Comment {
|
||||
id?: string;
|
||||
text: string;
|
||||
rating?: number;
|
||||
author: string;
|
||||
trail: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
iri?: string;
|
||||
expand?: {
|
||||
author: UserAnonymous
|
||||
author: Actor
|
||||
}
|
||||
|
||||
constructor(text: string, rating: number, author: string, trail: string) {
|
||||
constructor(text: string, author: string, trail: string) {
|
||||
this.text = text;
|
||||
this.rating = rating;
|
||||
this.author = author;
|
||||
this.trail = trail;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import type { User, UserAnonymous } from "./user";
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
|
||||
export class Follow {
|
||||
id?: string;
|
||||
follower: string;
|
||||
followee: string;
|
||||
status: "pending" | "accepted";
|
||||
expand?: {
|
||||
follower: UserAnonymous,
|
||||
followee: UserAnonymous
|
||||
follower: Actor,
|
||||
followee: Actor
|
||||
}
|
||||
|
||||
constructor(follower: string, followee: string) {
|
||||
this.follower = follower;
|
||||
this.followee = followee;
|
||||
this.status = "pending"
|
||||
}
|
||||
}
|
||||
@@ -72,19 +72,19 @@ export default class GPX {
|
||||
if (!Array.isArray(object.wpt)) {
|
||||
object.wpt = [object.wpt];
|
||||
}
|
||||
this.wpt = object.wpt.map(wpt => new Waypoint(wpt))
|
||||
this.wpt = object.wpt.filter(trk => typeof trk === 'object').map(wpt => new Waypoint(wpt))
|
||||
}
|
||||
if (object.rte) {
|
||||
if (!Array.isArray(object.rte)) {
|
||||
object.rte = [object.rte];
|
||||
}
|
||||
this.rte = object.rte.map(rte => new Route(rte))
|
||||
this.rte = object.rte.filter(trk => typeof trk === 'object').map(rte => new Route(rte))
|
||||
}
|
||||
if (object.trk) {
|
||||
if (!Array.isArray(object.trk)) {
|
||||
object.trk = [object.trk];
|
||||
}
|
||||
this.trk = object.trk.map(trk => new Track(trk))
|
||||
this.trk = object.trk.filter(trk => typeof trk === 'object').map(trk => new Track(trk))
|
||||
}
|
||||
|
||||
this.features = this.getTotals();
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import type { Trail } from "./trail";
|
||||
import type { ListShare } from "./list_share";
|
||||
import type { UserAnonymous } from "./user";
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
|
||||
export class List {
|
||||
id?: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
description?: string;
|
||||
elevation_gain?: number;
|
||||
elevation_loss?: number;
|
||||
distance?: number;
|
||||
duration?: number;
|
||||
avatar?: string;
|
||||
trails?: string[];
|
||||
iri?: string;
|
||||
expand?: {
|
||||
trails?: Trail[]
|
||||
list_share_via_list?: ListShare[]
|
||||
author?: UserAnonymous;
|
||||
author?: Actor;
|
||||
|
||||
}
|
||||
author: string;
|
||||
@@ -22,7 +28,7 @@ export class List {
|
||||
this.public = params?.public ?? false
|
||||
this.expand = { trails: trails };
|
||||
this.trails = trails.map(t => t.id!);
|
||||
this.description = params?.description;
|
||||
this.description = params?.description ?? "";
|
||||
this.avatar = params?.avatar;
|
||||
this.author = params?.author ?? "000000000000000";
|
||||
}
|
||||
@@ -35,35 +41,4 @@ export interface ListFilter {
|
||||
public?: boolean;
|
||||
shared?: boolean;
|
||||
sortOrder?: "+" | "-"
|
||||
}
|
||||
|
||||
export const enum ExpandType {
|
||||
None = 0,
|
||||
Trails = 1 << 0,
|
||||
Waypoints = 1 << 1,
|
||||
TrailCategories = 1 << 2,
|
||||
ListShares = 1 << 3,
|
||||
All = ~(~0 << 4),
|
||||
}
|
||||
|
||||
export function ExpandTypeToString(e: ExpandType) : string {
|
||||
|
||||
if (e == ExpandType.None)
|
||||
return "";
|
||||
|
||||
var ret = "";
|
||||
if ((e & ExpandType.Trails) === ExpandType.Trails) {
|
||||
ret += "trails,";
|
||||
}
|
||||
if ((e & ExpandType.Waypoints) === ExpandType.Waypoints) {
|
||||
ret += "trails.waypoints,";
|
||||
}
|
||||
if ((e & ExpandType.TrailCategories) === ExpandType.TrailCategories) {
|
||||
ret += "trails.category,";
|
||||
}
|
||||
if ((e & ExpandType.ListShares) === ExpandType.ListShares) {
|
||||
ret += "list_share_via_list,";
|
||||
}
|
||||
|
||||
return ret.slice(0, -1);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
import type { User } from "./user";
|
||||
|
||||
export class ListShare {
|
||||
id?: string;
|
||||
user: string;
|
||||
actor: string;
|
||||
list: string;
|
||||
permission: "view" | "edit"
|
||||
expand?: {
|
||||
user: User
|
||||
actor: Actor
|
||||
}
|
||||
|
||||
constructor(user: string, list: string, permission: "view" | "edit") {
|
||||
this.user = user;
|
||||
this.actor = user;
|
||||
this.list = list;
|
||||
this.permission = permission
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { UserAnonymous } from "./user";
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
|
||||
enum NotificationType {
|
||||
trailCreate = "trail_create",
|
||||
trailShare = "trail_share",
|
||||
listCreate = "list_create",
|
||||
trailLike = "trail_like",
|
||||
listShare = "list_share",
|
||||
newFollower = "new_follower",
|
||||
trailComment = "trail_comment"
|
||||
trailComment = "trail_comment",
|
||||
summitLogCreate = "summit_log_create",
|
||||
commentMention = "comment_mention",
|
||||
trailMention = "trail_mention",
|
||||
summitLogMention = "summit_log_mention",
|
||||
|
||||
};
|
||||
|
||||
interface Notification {
|
||||
@@ -18,9 +22,9 @@ interface Notification {
|
||||
author: string
|
||||
created: string;
|
||||
expand: {
|
||||
recipient: UserAnonymous;
|
||||
author: UserAnonymous;
|
||||
recipient: Actor;
|
||||
author: Actor;
|
||||
}
|
||||
}
|
||||
|
||||
export { type Notification, NotificationType }
|
||||
export { NotificationType, type Notification };
|
||||
|
||||
15
web/src/lib/models/profile.ts
Normal file
15
web/src/lib/models/profile.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { DateTime } from "activitypub-types";
|
||||
|
||||
export interface Profile {
|
||||
id: string;
|
||||
username: string;
|
||||
preferredUsername: string;
|
||||
acct: string;
|
||||
bio: string;
|
||||
followers: number;
|
||||
following: number;
|
||||
uri: string;
|
||||
icon: string;
|
||||
createdAt: DateTime;
|
||||
error?: string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
import type { Trail } from "./trail";
|
||||
import type { UserAnonymous } from "./user";
|
||||
|
||||
class SummitLog {
|
||||
id?: string;
|
||||
@@ -14,11 +14,14 @@ class SummitLog {
|
||||
elevation_loss?: number
|
||||
duration?: number
|
||||
author: string;
|
||||
trail?: string;
|
||||
iri?: string;
|
||||
created?: string;
|
||||
|
||||
expand?: {
|
||||
gpx_data?: string;
|
||||
trails_via_summit_logs?: Trail[];
|
||||
author?: UserAnonymous
|
||||
trail?: Trail;
|
||||
author?: Actor
|
||||
}
|
||||
|
||||
constructor(date: string, params?: { id?: string, text?: string, distance?: number, elevation_loss?: number, elevation_gain?: number, duration?: number, photos?: string[] }) {
|
||||
@@ -41,6 +44,7 @@ interface SummitLogFilter {
|
||||
category: string[],
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
trail?: string;
|
||||
}
|
||||
|
||||
export { SummitLog, type SummitLogFilter };
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
interface Activity {
|
||||
interface TimelineItem {
|
||||
id: string;
|
||||
trail_id: string;
|
||||
trail_author_username: string;
|
||||
trail_author_domain: string;
|
||||
trail_iri: string;
|
||||
name: string,
|
||||
description: string;
|
||||
date: string;
|
||||
@@ -15,4 +18,4 @@ interface Activity {
|
||||
created: string;
|
||||
}
|
||||
|
||||
export { type Activity }
|
||||
export { type TimelineItem }
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
import type { Category } from "./category";
|
||||
import type { Comment } from "./comment";
|
||||
import type { SummitLog } from "./summit_log";
|
||||
import type { Tag } from "./tag";
|
||||
import type { TrailLike } from "./trail_like";
|
||||
import type { TrailShare } from "./trail_share";
|
||||
import type { UserAnonymous } from "./user";
|
||||
import type { Waypoint } from "./waypoint";
|
||||
|
||||
class Trail {
|
||||
@@ -26,17 +27,21 @@ class Trail {
|
||||
category?: string;
|
||||
tags: string[];
|
||||
waypoints: string[];
|
||||
summit_logs: string[];
|
||||
polyline?: string;
|
||||
domain?: string;
|
||||
iri?: string;
|
||||
like_count: number;
|
||||
expand?: {
|
||||
tags?: Tag[]
|
||||
category?: Category;
|
||||
waypoints?: Waypoint[]
|
||||
summit_logs?: SummitLog[]
|
||||
author?: UserAnonymous
|
||||
summit_logs_via_trail?: SummitLog[]
|
||||
author?: Actor
|
||||
comments_via_trail?: Comment[]
|
||||
gpx_data?: string
|
||||
trail_share_via_trail?: TrailShare[]
|
||||
trail_like_via_trail?: TrailLike[]
|
||||
|
||||
}
|
||||
description?: string;
|
||||
author: string;
|
||||
@@ -83,13 +88,13 @@ class Trail {
|
||||
this.thumbnail = params?.thumbnail ?? 0;
|
||||
this.photos = params?.photos ?? [];
|
||||
this.waypoints = [];
|
||||
this.summit_logs = [];
|
||||
this.tags = []
|
||||
this.gpx = params?.gpx;
|
||||
this.like_count = 0
|
||||
this.expand = {
|
||||
category: params?.category,
|
||||
waypoints: params?.waypoints ?? [],
|
||||
summit_logs: params?.summit_logs ?? [],
|
||||
summit_logs_via_trail: params?.summit_logs ?? [],
|
||||
comments_via_trail: params?.comments ?? [],
|
||||
trail_share_via_trail: params?.shares ?? []
|
||||
}
|
||||
@@ -107,6 +112,7 @@ interface TrailFilter {
|
||||
author?: string;
|
||||
public?: boolean;
|
||||
shared?: boolean;
|
||||
private?: boolean;
|
||||
near: {
|
||||
lat?: number,
|
||||
lon?: number,
|
||||
@@ -124,6 +130,7 @@ interface TrailFilter {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
completed?: boolean;
|
||||
liked?: boolean;
|
||||
sort: "name" | "distance" | "elevation_gain" | "created";
|
||||
sortOrder: "+" | "-"
|
||||
}
|
||||
@@ -167,8 +174,12 @@ interface TrailSearchResult {
|
||||
public: boolean;
|
||||
thumbnail: string;
|
||||
polyline?: string;
|
||||
likes?: string[];
|
||||
like_count: number;
|
||||
shares?: string[];
|
||||
tags?: string[]
|
||||
domain?: string;
|
||||
iri?: string;
|
||||
gpx: string;
|
||||
_geo: {
|
||||
lat: number,
|
||||
@@ -195,12 +206,16 @@ export const defaultTrailSearchAttributes = [
|
||||
"created",
|
||||
"public",
|
||||
"thumbnail",
|
||||
"domain",
|
||||
"gpx",
|
||||
"tags",
|
||||
"like_count",
|
||||
"shares",
|
||||
"iri",
|
||||
"_geo",]
|
||||
|
||||
|
||||
export { Trail };
|
||||
|
||||
export type { TrailBoundingBox, TrailFilter, TrailFilterValues, TrailSearchResult };
|
||||
|
||||
|
||||
17
web/src/lib/models/trail_like.ts
Normal file
17
web/src/lib/models/trail_like.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
import type { Trail } from "./trail";
|
||||
|
||||
export class TrailLike {
|
||||
id?: string;
|
||||
actor: string;
|
||||
trail: string;
|
||||
expand?: {
|
||||
actor: Actor,
|
||||
trail?: Trail
|
||||
}
|
||||
|
||||
constructor(actor: string, trail: string) {
|
||||
this.actor = actor;
|
||||
this.trail = trail;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { User, UserAnonymous } from "./user";
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
|
||||
export class TrailShare {
|
||||
id?: string;
|
||||
user: string;
|
||||
actor: string;
|
||||
trail: string;
|
||||
permission: "view" | "edit"
|
||||
expand?: {
|
||||
user: UserAnonymous
|
||||
actor: Actor
|
||||
}
|
||||
|
||||
constructor(user: string, trail: string, permission: "view" | "edit") {
|
||||
this.user = user;
|
||||
constructor(actor: string, trail: string, permission: "view" | "edit") {
|
||||
this.actor = actor;
|
||||
this.trail = trail;
|
||||
this.permission = permission
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export type User = AuthRecord & {
|
||||
email?: string,
|
||||
password: string,
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
created?: string;
|
||||
}
|
||||
|
||||
@@ -15,6 +14,7 @@ export type UserAnonymous = {
|
||||
id: string,
|
||||
username?: string,
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
created?: string;
|
||||
private: boolean;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Activity } from "$lib/models/activity";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
|
||||
let activities: Activity[] = [];
|
||||
|
||||
export async function activities_index(author: string, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/activity?' + new URLSearchParams({
|
||||
"perPage": perPage.toString(),
|
||||
page: page.toString(),
|
||||
filter: `author="${author}"`,
|
||||
sort: "-created"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedActivities: ListResult<Activity> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...activities, ...fetchedActivities.items] : fetchedActivities.items
|
||||
|
||||
activities = result;
|
||||
|
||||
return { ...fetchedActivities, items: result };
|
||||
|
||||
}
|
||||
@@ -4,13 +4,22 @@ import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { currentUser } from "./user_store";
|
||||
import { isURL } from "$lib/util/file_util";
|
||||
|
||||
export const comments: Writable<Comment[]> = writable([])
|
||||
|
||||
export async function comments_index(trail: Trail) {
|
||||
let r = await fetch('/api/v1/comment?' + new URLSearchParams({
|
||||
filter: `trail="${trail.id}"`,
|
||||
sort: "-created"
|
||||
export async function comments_index(trailId: string, handle?: string) {
|
||||
let filter: string;
|
||||
if (isURL(trailId)) {
|
||||
filter = `trail="${trailId}"||trail.iri="${trailId}"||trail="${trailId.substring(trailId.length - 15)}"`
|
||||
} else {
|
||||
filter = `trail="${trailId}"`
|
||||
}
|
||||
let r = await fetch(`/api/v1/comment?` + new URLSearchParams({
|
||||
filter,
|
||||
expand: "author",
|
||||
sort: "-created",
|
||||
...(handle ? { handle } : {})
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -33,9 +42,11 @@ export async function comments_create(comment: Comment) {
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
|
||||
comment.author = user.authStore.record!.id;
|
||||
comment.author = user.actor
|
||||
|
||||
let r = await fetch('/api/v1/comment', {
|
||||
let r = await fetch('/api/v1/comment?' + new URLSearchParams({
|
||||
expand: "author",
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(comment),
|
||||
})
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import type { Follow } from "$lib/models/follow";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
|
||||
let follows: Follow[] = [];
|
||||
let follows: Actor[] = [];
|
||||
|
||||
export async function follows_index(data: { follower?: string, followee?: string }, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/follow?' + new URLSearchParams({
|
||||
filter: data.follower ? `follower='${data.follower}'` : data.followee ? `followee='${data.followee}'` : '',
|
||||
export async function follows_index(data: { username: string, type: "followers" | "following" }, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f(`/api/v1/follow?` + new URLSearchParams({
|
||||
handle: data.username,
|
||||
type: data.type,
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
requestKey: "page"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -19,11 +21,11 @@ export async function follows_index(data: { follower?: string, followee?: string
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedFollows: ListResult<Follow> = await r.json();
|
||||
const fetchedFollows: ListResult<Actor> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...follows, ...fetchedFollows.items] : fetchedFollows.items
|
||||
|
||||
follows = result;
|
||||
follows = result;
|
||||
|
||||
return { ...fetchedFollows, items: result };
|
||||
}
|
||||
@@ -45,25 +47,10 @@ export async function follows_a_b(a: string, b: string, f: (url: RequestInfo | U
|
||||
return response.items.at(0);
|
||||
}
|
||||
|
||||
export async function follows_counts(id: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/follow/count/' + id, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: { followers: number, following: number } = await r.json();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function follows_create(follow: Follow) {
|
||||
export async function follows_create(followee: string) {
|
||||
let r = await fetch('/api/v1/follow', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(follow),
|
||||
body: JSON.stringify({ followee }),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const shares: Writable<ListShare[]> = writable([])
|
||||
export async function list_share_index(list: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/list-share?' + new URLSearchParams({
|
||||
filter: `list='${list}'`,
|
||||
expand: "actor"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { ExpandType, ExpandTypeToString, List, type ListFilter } from "$lib/models/list";
|
||||
import { List, type ListFilter } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits } from "meilisearch";
|
||||
import { type AuthRecord, type ListResult } from "pocketbase";
|
||||
import { type AuthRecord, type ListResult, type RecordModel } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import type { ListSearchResult } from "./search_store";
|
||||
import { fetchGPX } from "./trail_store";
|
||||
import { currentUser } from "./user_store";
|
||||
import { objectToFormData } from "$lib/util/file_util";
|
||||
|
||||
let lists: List[] = []
|
||||
export const list: Writable<List | null> = writable(null)
|
||||
export const listTrail: Writable<Trail | null> = writable(null);
|
||||
|
||||
export async function lists_index(filter?: ListFilter, page: number = 1, perPage: number = 5,
|
||||
f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch,
|
||||
e: ExpandType = ExpandType.All) {
|
||||
f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const user = get(currentUser)
|
||||
|
||||
|
||||
const filterText = filter ? buildFilterText(user, filter) : ""
|
||||
|
||||
const r = await f('/api/v1/list?' + new URLSearchParams({
|
||||
@@ -23,7 +24,6 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
|
||||
perPage: perPage.toString(),
|
||||
page: page.toString(),
|
||||
filter: filterText,
|
||||
expand: ExpandTypeToString(e),
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -43,8 +43,8 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
|
||||
}
|
||||
|
||||
|
||||
export async function lists_search_filter(filter: ListFilter, page: number = 1, perPage: number = 5, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<ListResult<List>> {
|
||||
const user = get(currentUser)
|
||||
export async function lists_search_filter(filter: ListFilter, page: number = 1, perPage: number = 5, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch, user?: AuthRecord) {
|
||||
user ??= get(currentUser)
|
||||
|
||||
const filterText = buildSearchFilterText(user, filter)
|
||||
|
||||
@@ -65,43 +65,21 @@ export async function lists_search_filter(filter: ListFilter, page: number = 1,
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const searchResult: { page: number, totalPages: number, hits: Hits<Record<string, any>> } = await r.json();
|
||||
const result: { page: number, totalPages: number, hits: Hits<ListSearchResult> } = await r.json();
|
||||
|
||||
|
||||
const listIds = searchResult.hits.map((h: Record<string, any>) => h.id);
|
||||
const resultLists: List[] = await searchResultToLists(result.hits)
|
||||
|
||||
if (listIds.length == 0) {
|
||||
return { items: [], page: searchResult.page, perPage, totalItems: 0, totalPages: searchResult.totalPages };
|
||||
}
|
||||
return { items: resultLists, ...result };
|
||||
|
||||
r = await f('/api/v1/list?' + new URLSearchParams({
|
||||
expand: "trails,trails.waypoints,trails.category,list_share_via_list",
|
||||
filter: `'${listIds.join(',')}'~id`,
|
||||
sort: filter.sort && filter.sortOrder ? `${filter.sortOrder}${filter.sort}` : ''
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedLists: ListResult<List> = await r.json();
|
||||
|
||||
const result = page > 1 ? [...lists, ...fetchedLists.items] : fetchedLists.items
|
||||
|
||||
lists = result;
|
||||
|
||||
return { ...fetchedLists, items: result };
|
||||
|
||||
}
|
||||
|
||||
export async function lists_show(id: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch
|
||||
, e: ExpandType = ExpandType.All) {
|
||||
export async function lists_show(id: string, handle?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f(`/api/v1/list/${id}?` + new URLSearchParams({
|
||||
expand: ExpandTypeToString(e)
|
||||
expand: "author,trails,trails.author,list_share_via_list.actor",
|
||||
...(handle ? { handle } : {})
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -118,9 +96,6 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
|
||||
trail.expand.gpx_data = gpxData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
list.set(response);
|
||||
|
||||
return response;
|
||||
@@ -132,11 +107,17 @@ export async function lists_create(list: List, avatar?: File) {
|
||||
if (!user) {
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
list.author = user.id;
|
||||
list.author = user.actor;
|
||||
|
||||
let r = await fetch('/api/v1/list', {
|
||||
const formData = objectToFormData(list, ["expand"])
|
||||
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
let r = await fetch('/api/v1/list/form', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(list),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -146,45 +127,18 @@ export async function lists_create(list: List, avatar?: File) {
|
||||
|
||||
const model: List = await r.json();
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/list/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
return await r.json();
|
||||
return model;
|
||||
}
|
||||
|
||||
export async function lists_update(list: List, avatar?: File) {
|
||||
let r = await fetch('/api/v1/list/' + list.id, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(list),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const model: List = await r.json();
|
||||
|
||||
const formData = new FormData();
|
||||
const formData = objectToFormData(list)
|
||||
|
||||
if (avatar) {
|
||||
formData.append("avatar", avatar);
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/list/${model.id!}/file`, {
|
||||
let r = await fetch('/api/v1/list/form/' + list.id, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
@@ -193,6 +147,9 @@ export async function lists_update(list: List, avatar?: File) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const model: List = await r.json();
|
||||
return model;
|
||||
}
|
||||
|
||||
export async function lists_add_trail(list: List, trail: Trail) {
|
||||
@@ -275,17 +232,17 @@ function buildSearchFilterText(user: AuthRecord, filter: ListFilter): string {
|
||||
if (filter.public !== undefined) {
|
||||
filterText += `(public = ${filter.public}`
|
||||
|
||||
if (!filter.author?.length || filter.author == user?.id) {
|
||||
filterText += ` OR author = ${user?.id}`
|
||||
if (!filter.author?.length || filter.author == user?.actor) {
|
||||
filterText += ` OR author = ${user?.actor}`
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
|
||||
if (filter.shared !== undefined) {
|
||||
if (filter.shared === true) {
|
||||
filterText += ` OR shares = ${user?.id}`
|
||||
filterText += ` OR shares = ${user?.actor}`
|
||||
} else {
|
||||
filterText += ` AND NOT shares = ${user?.id}`
|
||||
filterText += ` AND NOT shares = ${user?.actor}`
|
||||
|
||||
}
|
||||
}
|
||||
@@ -293,4 +250,44 @@ function buildSearchFilterText(user: AuthRecord, filter: ListFilter): string {
|
||||
}
|
||||
|
||||
return filterText
|
||||
}
|
||||
|
||||
export async function searchResultToLists(hits: Hits<ListSearchResult>): Promise<List[]> {
|
||||
const lists: List[] = []
|
||||
for (const h of hits) {
|
||||
const l: List & RecordModel = {
|
||||
collectionId: "lists",
|
||||
collectionName: "lists",
|
||||
updated: new Date(h.created * 1000).toISOString(),
|
||||
author: h.author,
|
||||
name: h.name,
|
||||
public: h.public,
|
||||
description: h.description,
|
||||
id: h.id,
|
||||
trails: Array(h.trails).fill("000000000000000"),
|
||||
avatar: h.avatar,
|
||||
elevation_gain: h.elevation_gain,
|
||||
elevation_loss: h.elevation_loss,
|
||||
distance: h.distance,
|
||||
duration: h.duration,
|
||||
iri: h.iri,
|
||||
expand: {
|
||||
author: {
|
||||
icon: h.author_avatar,
|
||||
id: h.author,
|
||||
preferred_username: h.author_name,
|
||||
} as any,
|
||||
list_share_via_list: h.shares?.map(s => ({
|
||||
permission: "view",
|
||||
list: h.id,
|
||||
actor: s,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lists.push(l)
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export async function notifications_index(data: { recipient: string, seen?: bool
|
||||
const r = await f('/api/v1/notification?' + new URLSearchParams({
|
||||
filter: `created<=@month&&recipient='${data.recipient}'` + (data.seen !== undefined ? `&&seen=${data.seen}` : ''),
|
||||
sort: '+seen,-created',
|
||||
expand: 'author',
|
||||
page: page.toString(),
|
||||
"perPage": perPage.toString()
|
||||
}), {
|
||||
|
||||
135
web/src/lib/stores/profile_store.ts
Normal file
135
web/src/lib/stores/profile_store.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { TimelineItem } from "$lib/models/timeline";
|
||||
import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailSearchResult } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits } from "meilisearch";
|
||||
import type { ListResult } from "pocketbase";
|
||||
import { searchResultToTrailList } from "./trail_store";
|
||||
import type { SummitLog, SummitLogFilter } from "$lib/models/summit_log";
|
||||
import { buildFilterText } from "./summit_log_store";
|
||||
import type { ListFilter } from "$lib/models/list";
|
||||
import type { ListSearchResult } from "./search_store";
|
||||
import { searchResultToLists } from "./list_store";
|
||||
|
||||
let timeline: TimelineItem[] = []
|
||||
|
||||
|
||||
export async function profile_show(handle: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f('/api/v1/profile/' + handle, {
|
||||
method: 'GET',
|
||||
})
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response = await r.json()
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
export async function profile_lists_index(handle: string, filter: ListFilter, page: number = 1, perPage: number = 6, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/lists`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q: filter.q,
|
||||
options: {
|
||||
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
|
||||
hitsPerPage: perPage,
|
||||
page: page
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const result: { page: number, totalPages: number, hits: Hits<ListSearchResult> } = await r.json();
|
||||
|
||||
if (result.hits.length == 0) {
|
||||
return { items: [], ...result };
|
||||
}
|
||||
|
||||
const resultLists = await searchResultToLists(result.hits)
|
||||
|
||||
return { items: resultLists, ...result };
|
||||
|
||||
}
|
||||
|
||||
|
||||
export async function profile_timeline_index(handle: string, page: number, perPage: number = 1, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/timeline?` + new URLSearchParams({
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
sort: '-created'
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedTimeline: ListResult<TimelineItem> = await r.json()
|
||||
|
||||
const result = page > 1 ? [...timeline, ...fetchedTimeline.items] : fetchedTimeline.items
|
||||
|
||||
timeline = result;
|
||||
|
||||
return { ...fetchedTimeline, items: result };
|
||||
|
||||
}
|
||||
|
||||
export async function profile_trails_index(handle: string, filter: TrailFilter, page: number = 1, perPage: number = 1, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/trails`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
q: filter.q,
|
||||
options: {
|
||||
attributesToRetrieve: defaultTrailSearchAttributes,
|
||||
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
|
||||
hitsPerPage: 12,
|
||||
page: page
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const result: { page: number, totalPages: number, hits: Hits<TrailSearchResult> } = await r.json();
|
||||
|
||||
if (result.hits.length == 0) {
|
||||
return { items: [], ...result };
|
||||
}
|
||||
|
||||
const resultTrails: Trail[] = await searchResultToTrailList(result.hits)
|
||||
|
||||
return { items: resultTrails, ...result };
|
||||
|
||||
}
|
||||
|
||||
export async function profile_stats_index(handle: string, filter: SummitLogFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const filterText = buildFilterText(filter);
|
||||
|
||||
const r = await f(`/api/v1/profile/${handle}/stats?` + new URLSearchParams({
|
||||
filter: filterText,
|
||||
expand: "trail.category,author",
|
||||
sort: "+date",
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const result: SummitLog[] = await r.json();
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { defaultTrailSearchAttributes } from "$lib/models/trail";
|
||||
import type { Actor } from "$lib/models/activitypub/actor";
|
||||
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
||||
import type { ListResult } from "pocketbase";
|
||||
|
||||
export type LocationSearchResult = {
|
||||
name: string;
|
||||
@@ -12,37 +14,24 @@ export type LocationSearchResult = {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type TrailSearchResult = {
|
||||
id: string;
|
||||
_geo: {
|
||||
lat: number,
|
||||
lon: number
|
||||
}
|
||||
author: string;
|
||||
category: string;
|
||||
completed: boolean;
|
||||
created: number;
|
||||
date: number;
|
||||
description: string;
|
||||
difficulty: "easy" | "moderate" | "difficult"
|
||||
distance: number;
|
||||
duration: number
|
||||
elevation_gain: number;
|
||||
elevation_loss: number
|
||||
location: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
polyline?: string;
|
||||
}
|
||||
|
||||
export type ListSearchResult = {
|
||||
id: string;
|
||||
author: string;
|
||||
author_name: string;
|
||||
author_avatar: string;
|
||||
avatar?: string;
|
||||
created: number;
|
||||
description: string;
|
||||
name: string;
|
||||
elevation_gain: number;
|
||||
elevation_loss: number;
|
||||
distance: number,
|
||||
duration: number,
|
||||
domain?: string,
|
||||
public: boolean;
|
||||
trails: string[]
|
||||
trails: number
|
||||
shares?: string[];
|
||||
iri?: string;
|
||||
}
|
||||
|
||||
type NominatimResponse = {
|
||||
@@ -202,4 +191,21 @@ export async function searchMulti(options: MultiSearchParams): Promise<MultiSear
|
||||
|
||||
|
||||
return response.results
|
||||
}
|
||||
|
||||
export async function searchActors(q: string, includeSelf: boolean = true): Promise<Actor[]> {
|
||||
try {
|
||||
const r = await fetch(`/api/v1/search/actor?q=${q}&includeSelf=${includeSelf}`,)
|
||||
|
||||
if (!r.ok) {
|
||||
return []
|
||||
}
|
||||
const response: ListResult<Actor> = await r.json()
|
||||
|
||||
return response.items
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,19 @@ import { APIError } from "$lib/util/api_util";
|
||||
import { type AuthRecord, type ListResult } from "pocketbase";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { currentUser } from "./user_store";
|
||||
import { isURL, objectToFormData } from "$lib/util/file_util";
|
||||
|
||||
export const summitLog: Writable<SummitLog> = writable(new SummitLog(new Date().toISOString().substring(0, 10)));
|
||||
export const summitLogs: Writable<SummitLog[]> = writable([]);
|
||||
|
||||
export async function summit_logs_index(author: string, filter?: SummitLogFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
let filterText = `author='${author}'`
|
||||
filterText += filter ? "&&" + buildFilterText(filter) : "";
|
||||
export async function summit_logs_index(filter?: SummitLogFilter, handle?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f('/api/v1/summit-log?' + new URLSearchParams({
|
||||
filter: filterText,
|
||||
...(filter ? { filter: buildFilterText(filter) } : {}),
|
||||
perPage: "-1",
|
||||
expand: "trails_via_summit_logs.category",
|
||||
expand: "trail.category,author",
|
||||
sort: "+date",
|
||||
...(handle ? { handle } : {})
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -28,18 +27,6 @@ export async function summit_logs_index(author: string, filter?: SummitLogFilter
|
||||
|
||||
const fetchedSummitLogs: ListResult<SummitLog> = await r.json();
|
||||
|
||||
// for (const log of fetchedSummitLogs.items) {
|
||||
// if (!log.gpx) {
|
||||
// continue
|
||||
// }
|
||||
// const gpxData: string = await fetchGPX(log as any, f);
|
||||
|
||||
// if (!log.expand) {
|
||||
// log.expand = {};
|
||||
// }
|
||||
// log.expand.gpx_data = gpxData;
|
||||
// }
|
||||
|
||||
summitLogs.set(fetchedSummitLogs.items);
|
||||
|
||||
return fetchedSummitLogs;
|
||||
@@ -51,11 +38,27 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
|
||||
summitLog.author = user.id
|
||||
summitLog.author = user.actor
|
||||
|
||||
let r = await f('/api/v1/summit-log', {
|
||||
const formData = objectToFormData(summitLog, ["expand"])
|
||||
|
||||
if (summitLog._gpx && summitLog._gpx instanceof File) {
|
||||
formData.append("gpx", summitLog._gpx)
|
||||
}
|
||||
|
||||
|
||||
if (summitLog._photos && summitLog._photos.length) {
|
||||
|
||||
for (const photo of summitLog._photos) {
|
||||
formData.append("photos", photo)
|
||||
}
|
||||
}
|
||||
|
||||
let r = await f('/api/v1/summit-log/form?' + new URLSearchParams({
|
||||
expand: "author"
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(summitLog),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -65,43 +68,6 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
|
||||
|
||||
let model: SummitLog = await r.json();
|
||||
|
||||
if (summitLog._gpx && summitLog._gpx instanceof File) {
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append("gpx", summitLog._gpx)
|
||||
|
||||
r = await f(`/api/v1/summit-log/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
}
|
||||
|
||||
if (summitLog._photos && summitLog._photos.length) {
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const photo of summitLog._photos) {
|
||||
formData.append("photos", photo)
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/summit-log/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -111,19 +77,9 @@ export async function summit_logs_update(oldSummitLog: SummitLog, newSummitLog:
|
||||
throw Error("Unauthenticated")
|
||||
}
|
||||
|
||||
newSummitLog.author = user.id
|
||||
newSummitLog.author = user.actor
|
||||
|
||||
let r = await fetch('/api/v1/summit-log/' + newSummitLog.id, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(newSummitLog),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
const formData = objectToFormData(newSummitLog, ["expand"])
|
||||
|
||||
for (const photo of newSummitLog._photos ?? []) {
|
||||
formData.append("photos", photo)
|
||||
@@ -139,7 +95,9 @@ export async function summit_logs_update(oldSummitLog: SummitLog, newSummitLog:
|
||||
formData.append("gpx", newSummitLog._gpx);
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/summit-log/${newSummitLog.id!}/file`, {
|
||||
let r = await fetch('/api/v1/summit-log/form/' + newSummitLog.id + '?' + new URLSearchParams({
|
||||
expand: "author"
|
||||
}), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
@@ -165,11 +123,11 @@ export async function summit_logs_delete(summitLog: SummitLog) {
|
||||
|
||||
}
|
||||
|
||||
function buildFilterText(filter: SummitLogFilter,): string {
|
||||
export function buildFilterText(filter: SummitLogFilter,): string {
|
||||
let filterText: string = "";
|
||||
|
||||
if (filter.category.length > 0) {
|
||||
filterText += `trails_via_summit_logs.category!=null&&'${filter.category.join(",")}'~trails_via_summit_logs.category`;
|
||||
filterText += `trail.category!=null&&'${filter.category.join(",")}'~trail.category`;
|
||||
}
|
||||
|
||||
if (filter.startDate) {
|
||||
@@ -180,6 +138,17 @@ function buildFilterText(filter: SummitLogFilter,): string {
|
||||
filterText += `${filter.category.length || filter.startDate ? '&&' : ''}date<='${filter.endDate}'`
|
||||
}
|
||||
|
||||
if (filter.trail) {
|
||||
if (filter.category.length || filter.startDate || filter.endDate) {
|
||||
filterText += "&&"
|
||||
}
|
||||
if (isURL(filter.trail)) {
|
||||
filterText += `trail='${filter.trail}'||trail.iri='${filter.trail}'||trail='${filter.trail.substring(filter.trail.length - 15)}'`;
|
||||
} else {
|
||||
filterText += `trail='${filter.trail}'`
|
||||
}
|
||||
}
|
||||
|
||||
return filterText;
|
||||
|
||||
}
|
||||
56
web/src/lib/stores/trail_like_store.ts
Normal file
56
web/src/lib/stores/trail_like_store.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { TrailLike } from "$lib/models/trail_like";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { type ListResult } from "pocketbase";
|
||||
|
||||
export async function trail_like_index(data: { trail?: string, actor?: string }, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let filter = ""
|
||||
if (data.trail) {
|
||||
filter += `trail='${data.trail}'`
|
||||
} else if (data.actor) {
|
||||
filter += `actor='${data.actor}'`
|
||||
}
|
||||
|
||||
const r = await f('/api/v1/trail-like?' + new URLSearchParams({
|
||||
filter: filter,
|
||||
expand: "actor"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: ListResult<TrailLike> = await r.json();
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
export async function trail_like_create(like: TrailLike) {
|
||||
let r = await fetch('/api/v1/trail-like', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(like),
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: TrailLike = await r.json()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export async function trail_like_delete(like: TrailLike) {
|
||||
const r = await fetch('/api/v1/trail-like/delete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(like)
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,19 @@ import { writable, type Writable } from "svelte/store";
|
||||
export const shares: Writable<TrailShare[]> = writable([])
|
||||
|
||||
|
||||
export async function trail_share_index(data: { trail?: string, user?: string }, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
export async function trail_share_index(data: { trail?: string, actor?: string, actorIRI?: string }, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let filter = ""
|
||||
if (data.trail) {
|
||||
filter += `trail='${data.trail}'`
|
||||
} else if (data.actor) {
|
||||
filter += `actor='${data.actor}'`
|
||||
} else if (data.actorIRI) {
|
||||
filter += `actor.iri='${data.actorIRI}'`
|
||||
}
|
||||
|
||||
const r = await f('/api/v1/trail-share?' + new URLSearchParams({
|
||||
filter: data.trail ? `trail='${data.trail}'` : data.user ? `user='${data.user}'` : '',
|
||||
filter: filter,
|
||||
expand: "actor"
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailFilter
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import { deepEqual } from "$lib/util/deep_util";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { getFileURL, objectToFormData } from "$lib/util/file_util";
|
||||
import * as M from "maplibre-gl";
|
||||
import type { Hits } from "meilisearch";
|
||||
import { type AuthRecord, type ListResult, type RecordModel } from "pocketbase";
|
||||
@@ -22,7 +22,7 @@ export const editTrail: Writable<Trail> = writable(new Trail(""));
|
||||
export async function trails_index(perPage: number = 21, random: boolean = false, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/trail?' + new URLSearchParams({
|
||||
"perPage": perPage.toString(),
|
||||
expand: "category,waypoints,summit_logs,tags",
|
||||
expand: "category,waypoints,summit_logs_via_trail,tags",
|
||||
sort: random ? "@random" : "",
|
||||
}), {
|
||||
method: 'GET',
|
||||
@@ -50,9 +50,9 @@ export async function trails_recommend(size: number, f: (url: RequestInfo | URL,
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
const response: Trail[] = await r.json()
|
||||
const response: Hits<TrailSearchResult> = await r.json()
|
||||
|
||||
return response;
|
||||
return searchResultToTrailList(response);
|
||||
|
||||
}
|
||||
|
||||
@@ -132,9 +132,12 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
|
||||
|
||||
}
|
||||
|
||||
export async function trails_show(id: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
export async function trails_show(id: string, handle?: string, loadGPX?: boolean, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
|
||||
const r = await f(`/api/v1/trail/${id}?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||
expand: "category,waypoints,summit_logs_via_trail,summit_logs_via_trail.author,trail_share_via_trail.actor,trail_like_via_trail,tags,author",
|
||||
...(handle ? { handle } : {})
|
||||
|
||||
}), {
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -157,7 +160,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques
|
||||
response.expand.gpx_data = gpxData;
|
||||
|
||||
|
||||
for (const log of response.expand.summit_logs ?? []) {
|
||||
for (const log of response.expand.summit_logs_via_trail ?? []) {
|
||||
const gpxData: string = await fetchGPX(log, f);
|
||||
|
||||
if (!log.expand) {
|
||||
@@ -188,10 +191,6 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
}, f, user);
|
||||
trail.waypoints.push(model.id!);
|
||||
}
|
||||
for (const summitLog of trail.expand?.summit_logs ?? []) {
|
||||
const model = await summit_logs_create(summitLog, f);
|
||||
trail.summit_logs.push(model.id!);
|
||||
}
|
||||
for (const tag of trail.expand?.tags ?? []) {
|
||||
if (!tag.id) {
|
||||
const model = await tags_create(tag)
|
||||
@@ -201,34 +200,22 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
}
|
||||
}
|
||||
|
||||
trail.author = user.id
|
||||
trail.author = user.actor
|
||||
|
||||
let r = await f(`/api/v1/trail?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...trail }),
|
||||
})
|
||||
const formData = objectToFormData(trail)
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
|
||||
let model: Trail = await r.json();
|
||||
|
||||
const formData = new FormData()
|
||||
if (gpx) {
|
||||
formData.append("gpx", gpx);
|
||||
formData.set("gpx", gpx);
|
||||
}
|
||||
|
||||
for (const photo of photos) {
|
||||
formData.append("photos", photo)
|
||||
formData.set("photos", photo)
|
||||
}
|
||||
|
||||
r = await f(`/api/v1/trail/${model.id!}/file`, {
|
||||
method: 'POST',
|
||||
let r = await f(`/api/v1/trail/form?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs_via_trail,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
@@ -237,15 +224,19 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const modelWithFiles: Trail = await r.json();
|
||||
let model: Trail = await r.json();
|
||||
|
||||
model.photos = modelWithFiles.photos;
|
||||
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) {
|
||||
summitLog.trail = model.id!;
|
||||
await summit_logs_create(summitLog, f);
|
||||
}
|
||||
|
||||
return model;
|
||||
|
||||
}
|
||||
|
||||
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
|
||||
newTrail.author = oldTrail.author
|
||||
|
||||
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand?.waypoints ?? [], newTrail.expand?.waypoints ?? []);
|
||||
|
||||
@@ -269,15 +260,15 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
const success = await waypoints_delete(deletedWaypoint);
|
||||
}
|
||||
|
||||
const summitLogUpdates = compareObjectArrays<SummitLog>(oldTrail.expand?.summit_logs ?? [], newTrail.expand?.summit_logs ?? []);
|
||||
const summitLogUpdates = compareObjectArrays<SummitLog>(oldTrail.expand?.summit_logs_via_trail ?? [], newTrail.expand?.summit_logs_via_trail ?? []);
|
||||
|
||||
for (const summitLog of summitLogUpdates.added) {
|
||||
summitLog.trail = newTrail.id!
|
||||
const model = await summit_logs_create(summitLog);
|
||||
newTrail.summit_logs.push(model.id!);
|
||||
}
|
||||
|
||||
for (const updatedSummitLog of summitLogUpdates.updated) {
|
||||
const oldSummitLog = oldTrail.expand?.summit_logs?.find(w => w.id == updatedSummitLog.id);
|
||||
const oldSummitLog = oldTrail.expand?.summit_logs_via_trail?.find(w => w.id == updatedSummitLog.id);
|
||||
|
||||
const model = await summit_logs_update(oldSummitLog!, updatedSummitLog);
|
||||
}
|
||||
@@ -301,11 +292,30 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
newTrail.tags = newTrail.tags.filter(t => t != tag.id);
|
||||
}
|
||||
|
||||
let r = await fetch(`/api/v1/trail/${newTrail.id}?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs,trail_share_via_trail,tags",
|
||||
const formData = objectToFormData(newTrail, ["expand"])
|
||||
|
||||
if (gpx) {
|
||||
formData.append("gpx", gpx);
|
||||
}
|
||||
|
||||
if (photos) {
|
||||
for (const photo of photos) {
|
||||
formData.append("photos+", photo)
|
||||
}
|
||||
}
|
||||
|
||||
const deletedPhotos = oldTrail.photos.filter(oldPhoto => !newTrail.photos.find(newPhoto => newPhoto === oldPhoto));
|
||||
|
||||
for (const deletedPhoto of deletedPhotos) {
|
||||
formData.append("photos-", deletedPhoto.replace(/^.*[\\/]/, ''));
|
||||
}
|
||||
|
||||
|
||||
let r = await fetch(`/api/v1/trail/form/${newTrail.id}?` + new URLSearchParams({
|
||||
expand: "category,waypoints,summit_logs_via_trail,trail_share_via_trail,tags",
|
||||
}), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...newTrail, expand: undefined }),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -316,39 +326,12 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
|
||||
let model: Trail = await r.json();
|
||||
|
||||
const formData = new FormData()
|
||||
if (gpx) {
|
||||
formData.append("gpx", gpx);
|
||||
}
|
||||
|
||||
if (photos) {
|
||||
for (const photo of photos) {
|
||||
formData.append("photos", photo)
|
||||
for (const log of model.expand?.summit_logs_via_trail ?? []) {
|
||||
if (!log.expand) {
|
||||
log.expand = {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const deletedPhotos = oldTrail.photos.filter(oldPhoto => !newTrail.photos.find(newPhoto => newPhoto === oldPhoto));
|
||||
|
||||
for (const deletedPhoto of deletedPhotos) {
|
||||
formData.append("photos-", deletedPhoto.replace(/^.*[\\/]/, ''));
|
||||
}
|
||||
|
||||
r = await fetch(`/api/v1/trail/${newTrail.id!}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const response = await r.json();
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const response: Trail = await r.json();
|
||||
|
||||
model.photos = response.photos
|
||||
model.gpx = response.gpx
|
||||
|
||||
trail.set(model);
|
||||
|
||||
return model;
|
||||
@@ -361,8 +344,8 @@ export async function trails_delete(trail: Trail) {
|
||||
await waypoints_delete(waypoint);
|
||||
}
|
||||
}
|
||||
if (trail.expand?.summit_logs) {
|
||||
for (const summit_log of trail.expand.summit_logs) {
|
||||
if (trail.expand?.summit_logs_via_trail) {
|
||||
for (const summit_log of trail.expand.summit_logs_via_trail) {
|
||||
await summit_logs_delete(summit_log);
|
||||
}
|
||||
}
|
||||
@@ -455,14 +438,14 @@ export async function fetchGPX(trail: { gpx?: string } & Record<string, any>, f:
|
||||
return gpxData
|
||||
}
|
||||
|
||||
async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<Trail[]> {
|
||||
export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<Trail[]> {
|
||||
const trails: Trail[] = []
|
||||
for (const h of hits) {
|
||||
const t: Trail & RecordModel = {
|
||||
collectionId: "trails",
|
||||
collectionName: "trails",
|
||||
updated: new Date(h.created * 1000).toISOString(),
|
||||
author: h.author,
|
||||
author: h.author_name,
|
||||
name: h.name,
|
||||
photos: h.thumbnail ? [h.thumbnail] : [],
|
||||
public: h.public,
|
||||
@@ -484,26 +467,35 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<T
|
||||
location: h.location,
|
||||
gpx: h.gpx,
|
||||
polyline: h.polyline,
|
||||
domain: h.domain,
|
||||
iri: h.iri,
|
||||
thumbnail: 0,
|
||||
like_count: h.like_count,
|
||||
expand: {
|
||||
author: {
|
||||
collectionId: "users",
|
||||
private: false,
|
||||
collectionId: "activitypub_actors",
|
||||
isLocal: (h.domain?.length ?? 0) == 0,
|
||||
id: h.author,
|
||||
avatar: h.author_avatar,
|
||||
username: h.author_name
|
||||
icon: h.author_avatar,
|
||||
preferred_username: h.author_name,
|
||||
domain: h.domain,
|
||||
} as any,
|
||||
trail_share_via_trail: h.shares?.map(s => ({
|
||||
permission: "view",
|
||||
trail: h.id,
|
||||
user: s,
|
||||
actor: s,
|
||||
})),
|
||||
trail_like_via_trail: h.likes?.map(l => ({
|
||||
trail: h.id,
|
||||
actor: l,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
trails.push(t)
|
||||
}
|
||||
|
||||
return trails
|
||||
}
|
||||
|
||||
@@ -532,27 +524,64 @@ function buildFilterText(user: AuthRecord, filter: TrailFilter, includeGeo: bool
|
||||
filterText += ` AND author = ${filter.author}`
|
||||
}
|
||||
|
||||
if (filter.public !== undefined || filter.private !== undefined || filter.shared !== undefined) {
|
||||
filterText += " AND ("
|
||||
|
||||
const showPublic = filter.public === undefined || filter.public === true;
|
||||
const showPrivate = filter.private === undefined || filter.private === true;
|
||||
const showShared = filter.shared !== undefined && filter.shared === true;
|
||||
|
||||
if (showPublic === true) {
|
||||
filterText += "(public = TRUE";
|
||||
if (showPrivate === true && (!filter.author?.length || filter.author == user?.actor)) {
|
||||
filterText += ` OR author = ${user?.actor}`;
|
||||
}
|
||||
filterText += ")";
|
||||
}
|
||||
else if (!filter.author?.length || filter.author == user?.actor) {
|
||||
filterText += "public = FALSE";
|
||||
filterText += ` AND author = ${user?.actor}`;
|
||||
}
|
||||
|
||||
if (filter.shared !== undefined) {
|
||||
if (filter.shared === true) {
|
||||
filterText += ` OR shares = ${user?.actor}`
|
||||
} else {
|
||||
filterText += ` AND NOT shares = ${user?.actor}`
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
filterText += ")";
|
||||
}
|
||||
|
||||
/*
|
||||
if (filter.public !== undefined || filter.shared !== undefined) {
|
||||
filterText += " AND ("
|
||||
if (filter.public !== undefined) {
|
||||
filterText += `(public = ${filter.public}`
|
||||
|
||||
if (!filter.author?.length || filter.author == user?.id) {
|
||||
filterText += ` OR author = ${user?.id}`
|
||||
if (!filter.author?.length || filter.author == user?.actor) {
|
||||
filterText += ` OR author = ${user?.actor}`
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
|
||||
if (filter.shared !== undefined) {
|
||||
if (filter.shared === true) {
|
||||
filterText += ` OR shares = ${user?.id}`
|
||||
filterText += ` OR shares = ${user?.actor}`
|
||||
} else {
|
||||
filterText += ` AND NOT shares = ${user?.id}`
|
||||
filterText += ` AND NOT shares = ${user?.actor}`
|
||||
|
||||
}
|
||||
}
|
||||
filterText += ")"
|
||||
}
|
||||
*/
|
||||
|
||||
if (filter.liked === true) {
|
||||
filterText += ` AND likes = ${user?.actor}`
|
||||
}
|
||||
|
||||
if (filter.startDate) {
|
||||
filterText += ` AND date >= ${new Date(filter.startDate).getTime() / 1000}`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user