adds notifications

This commit is contained in:
Christian Beutel
2024-12-21 03:24:36 +01:00
parent 034f6873c1
commit 9140b523da
39 changed files with 1613 additions and 142 deletions

View File

@@ -0,0 +1,14 @@
<div
class="skeleton-notification-card animate-pulse flex items-center gap-x-3 px-3 py-2 m-2 bg-menu-background rounded-xl"
>
<!-- Avatar placeholder -->
<div
class="h-8 w-8 bg-menu-item-background-focus rounded-full shrink-0"
></div>
<!-- Text placeholders -->
<div class="basis-full space-y-2">
<div class="h-4 bg-menu-item-background-focus rounded"></div>
<div class="h-4 bg-menu-item-background-focus rounded w-2/3"></div>
</div>
</div>

View File

@@ -1,8 +1,16 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let name: string = "";
export let value: boolean = false;
export let label: string = "";
export let error: string = "";
const dispatch = createEventDispatcher();
function handleToggleChange() {
dispatch("change", value);
}
</script>
<label class="relative my-2 inline-flex items-center cursor-pointer">
@@ -12,6 +20,7 @@
type="checkbox"
class="sr-only peer"
value="1"
on:change={handleToggleChange}
/>
<div
class="w-11 h-6 bg-input-background border border-input-border peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-input-ring rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"

View File

@@ -35,20 +35,20 @@
</script>
<div
class="flex gap-4 items-center"
class="flex gap-4 items-start"
in:fade={{ duration: 150 }}
out:fade={{ duration: 150 }}
>
{#if comment.expand?.author.private}
<img
class="rounded-full w-10 aspect-square"
class="rounded-full w-10 aspect-square shrink-0"
src={avatarSrc}
alt="avatar"
/>
{:else}
<a
href="/profile/{comment.expand?.author.id}"
class="text-sm font-semibold"
class="text-sm font-semibold shrink-0"
>
<img
class="rounded-full w-10 aspect-square"
@@ -97,7 +97,7 @@
<TextField extraClasses="mt-2" bind:value={editedComment}
></TextField>
{:else}
<p>{comment.text}</p>
<p class="whitespace-pre-wrap text-sm">{comment.text}</p>
{/if}
</div>
</div>

View File

@@ -4,12 +4,15 @@
import { theme, toggleTheme } from "$lib/stores/theme_store";
import { currentUser, logout } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n";
import { backInOut, cubicOut } from "svelte/easing";
import { tweened } from "svelte/motion";
import Drawer from "./base/drawer.svelte";
import Dropdown from "./base/dropdown.svelte";
import LogoTextLight from "./logo/logo_text_light.svelte";
import { _, format } from "svelte-i18n";
import { page } from "$app/stores";
import NotificationCard from "./notification/notification_card.svelte";
import NotificationDropdown from "./notification/notification_dropdown.svelte";
let navBarItems = [
{ text: "Home", value: "/" },
@@ -200,6 +203,7 @@
<a class="btn-primary btn-large" href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-trail")}</a
>
<NotificationDropdown></NotificationDropdown>
<Dropdown
items={dropdownItems}
on:change={(e) => handleDropdownClick(e.detail)}

View File

@@ -0,0 +1,126 @@
<script lang="ts">
import {
NotificationType,
type Notification,
} from "$lib/models/notification";
import { getFileURL } from "$lib/util/file_util";
import { formatTimeSince } from "$lib/util/format_util";
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
export let notification: Notification;
const dispatch = createEventDispatcher();
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 timeSince = formatTimeSince(new Date(notification.created ?? ""));
$: title = getTitle(notification);
$: description = getDescription(notification);
$: link = getLink(notification);
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 },
});
case NotificationType.newFollower:
return $_("notification-new-follower");
case NotificationType.trailComment:
return $_("notification-trail-comment", {
values: {
user: n.expand.author.username,
trail: n.metadata?.trail,
},
});
case NotificationType.trailCreate:
return $_("notification-new-trail", {
values: { user: n.expand.author.username },
});
case NotificationType.trailShare:
return $_("notification-trail-share", {
values: { user: n.expand.author.username },
});
}
}
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 ?? "";
}
}
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}`;
case NotificationType.newFollower:
return n.expand.author.private === true
? null
: `/profile/${n.author}`;
case NotificationType.trailComment:
return `/trail/view/${n.metadata?.id}`;
case NotificationType.trailCreate:
return `/trail/view/${n.metadata?.id}`;
case NotificationType.trailShare:
return `/trail/view/${n.metadata?.id}`;
}
}
function handleItemClick() {
dispatch("click", { notification, link });
}
</script>
<li
class="flex items-center gap-x-3 px-3 py-2 hover:bg-menu-item-background-hover relative cursor-pointer"
role="presentation"
on:click={handleItemClick}
>
<img class="rounded-full w-8 aspect-square" src={avatarSrc} alt="avatar" />
<div>
<p
class="text-sm {notification.seen
? 'font-medium'
: 'font-semibold'} mr-3"
>
{title}:
</p>
<p class="text-sm line-clamp-1">{description}</p>
<p class="text-xs text-gray-500">
{$_(`n-${timeSince.unit}-ago`, {
values: { n: timeSince.value },
})}
</p>
</div>
{#if !notification.seen}
<div
class="bg-content w-[6px] aspect-square rounded-full absolute top-3 right-3"
></div>
{/if}
</li>

View File

@@ -0,0 +1,141 @@
<script lang="ts">
import type { Notification } from "$lib/models/notification";
import { fly } from "svelte/transition";
import NotificationCard from "./notification_card.svelte";
import { page } from "$app/stores";
import {
notifications_index,
notifications_mark_as_seen,
} from "$lib/stores/notification_store";
import { currentUser } from "$lib/stores/user_store";
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import SkeletonListItem from "../base/skeleton_list_item.svelte";
import SkeletonNotificationCard from "../base/skeleton_notification_card.svelte";
let notifications: Notification[] = [];
const pagination = {
page: $page.data.notifications.page,
totalPages: $page.data.notifications.totalPages,
};
let loadingNextPage: boolean = false;
let isOpen = false;
$: unreadCount = notifications.reduce(
(value, n) => (value += n.seen ? 0 : 1),
0,
);
onMount(() => {
if (!notifications.length && $page.data.notifications?.items?.length) {
notifications = $page.data.notifications.items;
}
});
async function toggleMenu(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
isOpen = !isOpen;
pagination.page = 0;
await loadNextPage();
}
function handleWindowClick(e: MouseEvent) {
if (
(e.target as HTMLElement).parentElement?.classList.contains(
"dropdown-toggle",
)
) {
return;
}
isOpen = false;
}
async function onListScroll(e: Event) {
const container = e.target as HTMLDivElement;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
if (
scrollTop + clientHeight >= scrollHeight * 0.8 &&
pagination.page !== pagination.totalPages &&
!loadingNextPage
) {
await loadNextPage();
}
}
async function loadNextPage() {
loadingNextPage = true;
if (!$currentUser) {
return;
}
pagination.page += 1;
const result = await notifications_index(
{ recipient: $currentUser.id },
pagination.page,
);
notifications = result.items;
loadingNextPage = false;
}
async function handleNotificationClick(e: CustomEvent) {
await notifications_mark_as_seen(e.detail.notification);
e.detail.notification.seen = true;
notifications = notifications;
if (e.detail.link) {
goto(e.detail.link);
}
}
</script>
<svelte:window on:mouseup={handleWindowClick} />
<div class="dropdown relative">
{#if unreadCount > 0}
<div
class="absolute -top-1 -right-1 text-sm rounded-full bg-content text-content-inverse w-4 aspect-square text-center"
>
{unreadCount}
</div>
{/if}
<div class="dropdown-toggle">
<button on:click={toggleMenu} class="btn-icon">
<i class="fa fa-bell"></i>
</button>
</div>
{#if isOpen}
<ul
class="menu absolute bg-menu-background border border-input-border rounded-l-xl rounded-b-xl shadow-md right-0 overflow-scroll mt-4 max-h-96 w-64"
class:none={isOpen}
on:scroll={onListScroll}
style="z-index: 1001"
in:fly={{ y: -10, duration: 150 }}
out:fly={{ y: -10, duration: 150 }}
>
{#if loadingNextPage}
{#each { length: 5 } as _, index}
<SkeletonNotificationCard></SkeletonNotificationCard>
{/each}
{:else}
{#each notifications as notification}
<NotificationCard
on:click={handleNotificationClick}
{notification}
></NotificationCard>
{/each}
{/if}
</ul>
{/if}
</div>
<style>
</style>

View File

@@ -140,7 +140,7 @@
"link-copied": "Link kopiert",
"list": "{n, plural, =1 {Liste} other {Listen}}",
"list-not-shared": "Mit niemandem geteilt",
"list-public-warning": "Alle routen in dieser liste werden veröffentlicht.",
"list-public-warning": "Alle routen in dieser Liste werden veröffentlicht.",
"list-saved-successfully": "Liste gespeichert",
"list-share-warning": "Durch das Teilen einer Liste werden automatisch alle darin enthaltenen Routen freigegeben.",
"list-share-warning-update": "Hinzugefügte Routen werden mit allen geteilt, die Zugriff auf diese Liste haben.",
@@ -177,6 +177,13 @@
"no-results": "Keine Ergebnisse gefunden",
"not-a-valid-email-address": "Keine gültige Email-Adresse",
"not-completed": "Nicht abgeschlossen",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "Aus",
"only-me": "",
"or": "oder",
@@ -213,6 +220,12 @@
"search-trails": "Route suchen",
"select-list": "Liste auswählen",
"settings": "Einstellungen",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "No results found",
"not-a-valid-email-address": "Not a valid email address",
"not-completed": "Not completed",
"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-new-trail": "{user} created a new trail",
"notification-trail-comment": "{user} left a comment on your trail \"{trail}\"",
"notification-trail-share": "{user} shared a trail with you",
"notifications": "Notifications",
"off": "Off",
"only-me": "Only me",
"or": "or",
@@ -213,6 +220,12 @@
"search-trails": "Search trails",
"select-list": "Select List",
"settings": "Settings",
"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.",

View File

@@ -177,6 +177,13 @@
"no-results": "Pas de résultat",
"not-a-valid-email-address": "Adresse email invalide",
"not-completed": "Pas terminé",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "",
"only-me": "",
"or": "ou",
@@ -213,6 +220,12 @@
"search-trails": "Chercher un itinéraire",
"select-list": "Liste de choix",
"settings": "Paramètres",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "Nincs eredmény",
"not-a-valid-email-address": "Érvénytelen e-mail cím",
"not-completed": "Nem teljesített",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "",
"only-me": "",
"or": "vagy",
@@ -213,6 +220,12 @@
"search-trails": "Nyomvonalak keresése",
"select-list": "Lista kiválasztása",
"settings": "Beállítások",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "Nessun risultato trovato",
"not-a-valid-email-address": "Indirizzo email non valido",
"not-completed": "Non completato",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "Spento",
"only-me": "",
"or": "o",
@@ -213,6 +220,12 @@
"search-trails": "Cerca percorsi",
"select-list": "Seleziona lista",
"settings": "Impostazioni",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "Er zijn geen zoekresultaten",
"not-a-valid-email-address": "Het e-mailadres is ongeldig",
"not-completed": "Niet voltooid",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "",
"only-me": "",
"or": "of",
@@ -213,6 +220,12 @@
"search-trails": "Zoeken naar wandelroutes",
"select-list": "Kies een lijst",
"settings": "Instellingen",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "Brak wyników",
"not-a-valid-email-address": "Nieprawidłowy adres email",
"not-completed": "Nie dokończono",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "",
"only-me": "",
"or": "lub",
@@ -213,6 +220,12 @@
"search-trails": "Szukaj ścieżek",
"select-list": "Wybierz Listę",
"settings": "Ustawienia",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "Nenhum resultado encontrado",
"not-a-valid-email-address": "Não um endereço de e-mail válido",
"not-completed": "Não preenchido",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "",
"only-me": "",
"or": "ou",
@@ -213,6 +220,12 @@
"search-trails": "Procurar trilhos",
"select-list": "Selecionar lista",
"settings": "Definições",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -177,6 +177,13 @@
"no-results": "没有找到结果",
"not-a-valid-email-address": "无效电子邮箱地址",
"not-completed": "未完成",
"notification-list-create": "",
"notification-list-share": "",
"notification-new-follower": "",
"notification-new-trail": "",
"notification-trail-comment": "",
"notification-trail-share": "",
"notifications": "",
"off": "",
"only-me": "",
"or": "或",
@@ -213,6 +220,12 @@
"search-trails": "搜索路线",
"select-list": "选择列表",
"settings": "设置",
"settings-notification-list-create": "",
"settings-notification-list-share": "",
"settings-notification-new-follower": "",
"settings-notification-trail-comment": "",
"settings-notification-trail-create": "",
"settings-notification-trail-share": "",
"settings-privacy-account-private": "",
"settings-privacy-account-public": "",
"settings-privacy-lists-private": "",

View File

@@ -0,0 +1,26 @@
import type { UserAnonymous } from "./user";
enum NotificationType {
trailCreate = "trail_create",
trailShare = "trail_share",
listCreate = "list_create",
listShare = "list_share",
newFollower = "new_follower",
trailComment = "trail_comment"
};
interface Notification {
id: string;
type: NotificationType;
metadata?: Record<string, any>;
seen: boolean;
recipient: string
author: string
created: string;
expand: {
recipient: UserAnonymous;
author: UserAnonymous;
}
}
export { type Notification, NotificationType }

View File

@@ -1,26 +1,28 @@
import type { NotificationType } from "./notification";
class Settings {
id?: string;
unit?: "metric" | "imperial";
language?: "en" | "de" | "fr" | "hu"| "it" | "nl" | "pl" | "pt" | "zh";
language?: "en" | "de" | "fr" | "hu" | "it" | "nl" | "pl" | "pt" | "zh";
mapFocus?: "trails" | "location";
location?: { name: string, lat: number, lon: number };
category?: string;
tilesets?: {name: string, url: string}[]
terrain?: { terrain: string, hillshading: string};
tilesets?: { name: string, url: string }[]
terrain?: { terrain: string, hillshading: string };
user?: string;
privacy?: {account: "public" | "private", trails: "public" | "private", lists: "public" | "private"}
privacy?: { account: "public" | "private", trails: "public" | "private", lists: "public" | "private" }
notifications?: Record<NotificationType, { web: boolean, email: boolean }>
constructor(
unit: "metric" | "imperial",
language: "en" | "de" | "fr" | "hu"| "it" | "nl" | "pl" | "pt" | "zh",
language: "en" | "de" | "fr" | "hu" | "it" | "nl" | "pl" | "pt" | "zh",
mapFocus: "trails" | "location",
user: string,
params?: {
location?: { name: string, lat: number, lon: number }
category?: string
tilesets?: {name: string, url: string}[]
terrain?: { terrain: string, hillshading: string};
tilesets?: { name: string, url: string }[]
terrain?: { terrain: string, hillshading: string };
}
) {
this.unit = unit;

View File

@@ -0,0 +1,38 @@
import type { Notification } from "$lib/models/notification";
import { ClientResponseError, type ListResult } from "pocketbase";
let notifications: Notification[] = [];
export async function notifications_index(data: { recipient: string, seen?: boolean }, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/notification?' + new URLSearchParams({
filter: `created>=@month&&recipient='${data.recipient}'` + (data.seen !== undefined ? `&&seen=${data.seen}` : ''),
sort: '+seen,-created',
page: page.toString(),
"per-page": perPage.toString()
}), {
method: 'GET',
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const fetchedNotifications: ListResult<Notification> = await r.json();
const result = page > 1 ? [...notifications, ...fetchedNotifications.items] : fetchedNotifications.items
notifications = result;
return { ...fetchedNotifications, items: result };
}
export async function notifications_mark_as_seen(notification: Notification) {
let r = await fetch('/api/v1/notification/' + notification.id, {
method: 'POST',
body: JSON.stringify(notification),
})
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
}

View File

@@ -3,7 +3,13 @@ import '$lib/i18n';
import type { LayoutServerLoad } from './$types';
import { env } from "$env/dynamic/private";
import type { Settings } from '$lib/models/settings';
import { notifications_index } from '$lib/stores/notification_store';
export const load: LayoutServerLoad = async ({ locals, url }) => {
return { settings: locals.settings as Settings, origin: env.ORIGIN }
export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
let notifications
if (locals.user) {
notifications = await notifications_index({ recipient: locals.user.id }, 1, 10, fetch);
}
return { settings: locals.settings as Settings, notifications, origin: env.ORIGIN }
}

View File

@@ -1,5 +1,5 @@
import type { Follow } from '$lib/models/follow';
import type { User } from '$lib/models/user';
import type { UserAnonymous } from '$lib/models/user';
import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit';
@@ -21,8 +21,8 @@ export async function GET(event: RequestEvent) {
.getList<Follow>(parseInt(page), parseInt(perPage), { sort: sort ?? "", filter: filter ?? "", requestKey: filter })
}
for (const follow of r.items) {
const follower = await pb.collection('users_anonymous').getOne<User>(follow.follower, {requestKey: filter})
const followee = await pb.collection('users_anonymous').getOne<User>(follow.followee, {requestKey: filter})
const follower = await pb.collection('users_anonymous').getOne<UserAnonymous>(follow.follower, { requestKey: filter })
const followee = await pb.collection('users_anonymous').getOne<UserAnonymous>(follow.followee, { requestKey: filter })
follow.expand = {
follower, followee
}

View File

@@ -0,0 +1,34 @@
import type { Notification } from '$lib/models/notification';
import type { UserAnonymous } from '$lib/models/user';
import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
const page = event.url.searchParams.get("page") ?? "0";
const perPage = event.url.searchParams.get("per-page") ?? "10";
const sort = event.url.searchParams.get('sort') ?? ""
const filter = event.url.searchParams.get("filter") ?? "";
try {
let r;
if (parseInt(perPage) < 0) {
r = {
items: await pb.collection('notifications')
.getFullList<Notification>({ sort: sort, filter: filter, requestKey: filter })
}
} else {
r = await pb.collection('notifications')
.getList<Notification>(parseInt(page), parseInt(perPage), { sort: sort ?? "", filter: filter ?? "", requestKey: filter })
}
for (const notification of r.items) {
const recipient = await pb.collection('users_anonymous').getOne<UserAnonymous>(notification.recipient, { requestKey: filter })
const author = await pb.collection('users_anonymous').getOne<UserAnonymous>(notification.author, { requestKey: filter })
notification.expand = {
recipient, author
}
}
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}

View File

@@ -0,0 +1,15 @@
import type { Follow } from "$lib/models/follow";
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
export async function POST(event: RequestEvent) {
const data = await event.request.json()
try {
const r = await pb.collection('notifications').update<Notification>(event.params.id as string, { ...data, seen: true })
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { goto, invalidate, invalidateAll } from "$app/navigation";
import { page } from "$app/stores";
import { env } from "$env/dynamic/public";
import Button from "$lib/components/base/button.svelte";
@@ -45,7 +45,7 @@
loading = true;
try {
await login(newUser);
goto($page.url.searchParams.get("r") ?? "/");
window.location.href = $page.url.searchParams.get("r") ?? "/";
} catch (e) {
if (
e instanceof ClientResponseError &&

View File

@@ -13,6 +13,7 @@
text: $_("language") + " & " + $_("units"),
value: "/settings/language",
},
{ text: $_("notifications"), value: "/settings/notifications" },
{ text: $_("map"), value: "/settings/map" },
{ text: `${$_("import")}/${$_("export")}`, value: "/settings/export" },
{

View File

@@ -0,0 +1,99 @@
<script lang="ts">
import { page } from "$app/stores";
import Toggle from "$lib/components/base/toggle.svelte";
import { NotificationType } from "$lib/models/notification";
import type { Settings } from "$lib/models/settings";
import { settings_update } from "$lib/stores/settings_store";
import { _ } from "svelte-i18n";
const notifications = ($page.data.settings as Settings)?.notifications ?? {
list_create: {
web: true,
email: true,
},
list_share: {
web: true,
email: true,
},
trail_create: {
web: true,
email: true,
},
trail_share: {
web: true,
email: true,
},
new_follower: {
web: true,
email: true,
},
trail_comment: {
web: true,
email: true,
},
};
const notificationItems: { text: string; key: NotificationType }[] = [
{
text: $_("notification-trail-comment"),
key: NotificationType.trailComment,
},
{
text: $_("notification-new-follower"),
key: NotificationType.newFollower,
},
{
text: $_("notification-trail-create"),
key: NotificationType.trailCreate,
},
{
text: $_("notification-trail-share"),
key: NotificationType.trailShare,
},
{
text: $_("notification-list-create"),
key: NotificationType.listCreate,
},
{
text: $_("notification-list-share"),
key: NotificationType.listShare,
},
];
async function updateNotificationSettings() {
await settings_update({
id: $page.data.settings!.id,
notifications,
});
}
</script>
<svelte:head>
<title>{$_("settings")} | wanderer</title>
</svelte:head>
<h2 class="text-2xl font-semibold">{$_("notifications")}</h2>
<hr class="mt-4 mb-6 border-input-border" />
<div
class="grid gap-4"
style="grid-template-columns: 1fr min-content min-content;"
>
<div></div>
<span class="text-sm font-medium">Web</span>
<span class="text-sm font-medium">Email</span>
{#each notificationItems as item}
<p>{item.text}</p>
<div>
<Toggle
on:change={updateNotificationSettings}
bind:value={notifications[item.key].web}
></Toggle>
</div>
<div>
<Toggle
on:change={updateNotificationSettings}
bind:value={notifications[item.key].email}
></Toggle>
</div>
{/each}
</div>

View File

@@ -257,6 +257,7 @@
$form.id = prevId;
$form.expand.gpx_data = gpxData;
$form.category = $page.data.settings.category || $categories[0].id;
$form.public = $page.data.settings?.privacy.trails === "public";
const log = new SummitLog(parseResult.trail.date as string, {
distance: $form.distance,