feat(chronik): rename route and heading to Aktivitäten
/chronik → /aktivitaeten; heading updated in all three locales. Component folder (lib/components/chronik/) stays unchanged — internal implementation detail, not user-facing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
127
frontend/src/routes/aktivitaeten/+page.svelte
Normal file
127
frontend/src/routes/aktivitaeten/+page.svelte
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page, navigating } from '$app/state';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import { notificationStore, type NotificationItem } from '$lib/stores/notifications.svelte';
|
||||
import ChronikFuerDichBox from '$lib/components/chronik/ChronikFuerDichBox.svelte';
|
||||
import ChronikFilterPills from '$lib/components/chronik/ChronikFilterPills.svelte';
|
||||
import ChronikTimeline from '$lib/components/chronik/ChronikTimeline.svelte';
|
||||
import ChronikEmptyState from '$lib/components/chronik/ChronikEmptyState.svelte';
|
||||
import ChronikErrorCard from '$lib/components/chronik/ChronikErrorCard.svelte';
|
||||
import type { components } from '$lib/generated/api';
|
||||
import type { FilterValue } from './+page.server';
|
||||
import { applyClientFilter } from './clientFilter';
|
||||
|
||||
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
filter: FilterValue;
|
||||
activityFeed: ActivityFeedItemDTO[];
|
||||
unreadNotifications: components['schemas']['NotificationDTO'][];
|
||||
loadError: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const { data }: Props = $props();
|
||||
|
||||
// Prefer the live SSE singleton for unread items so newly arriving mentions
|
||||
// prepend without a reload. On first mount, seed from the server-loaded unread
|
||||
// set if the singleton hasn't populated yet.
|
||||
onMount(() => {
|
||||
notificationStore.init();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
notificationStore.destroy();
|
||||
});
|
||||
|
||||
const liveUnread = $derived<NotificationItem[]>(
|
||||
notificationStore.notifications.filter((n) => !n.read)
|
||||
);
|
||||
|
||||
const seedUnread = $derived<NotificationItem[]>(
|
||||
data.unreadNotifications
|
||||
.filter((n): n is typeof n & { documentId: string; referenceId: string } =>
|
||||
Boolean(n.documentId && n.referenceId)
|
||||
)
|
||||
.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
documentId: n.documentId,
|
||||
documentTitle: n.documentTitle ?? null,
|
||||
referenceId: n.referenceId,
|
||||
annotationId: n.annotationId ?? null,
|
||||
read: n.read,
|
||||
createdAt: n.createdAt,
|
||||
actorName: n.actorName ?? ''
|
||||
}))
|
||||
);
|
||||
|
||||
// If the singleton has any data (including zero after mark-all), trust it;
|
||||
// otherwise fall back to the SSR-seeded unread set.
|
||||
const unread = $derived<NotificationItem[]>(
|
||||
notificationStore.notifications.length > 0 ? liveUnread : seedUnread
|
||||
);
|
||||
|
||||
async function onFilterChange(v: FilterValue) {
|
||||
const url = new URL(page.url);
|
||||
if (v === 'alle') url.searchParams.delete('filter');
|
||||
else url.searchParams.set('filter', v);
|
||||
await goto(`${url.pathname}${url.search}`, {
|
||||
keepFocus: true,
|
||||
noScroll: true,
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
|
||||
async function onMarkRead(n: NotificationItem) {
|
||||
await notificationStore.markRead(n);
|
||||
}
|
||||
|
||||
async function onMarkAllRead() {
|
||||
await notificationStore.markAllRead();
|
||||
}
|
||||
|
||||
const displayFeed = $derived(applyClientFilter(data.activityFeed, data.filter));
|
||||
|
||||
const isEmpty = $derived(displayFeed.length === 0);
|
||||
const emptyVariant = $derived<'first-run' | 'filter-empty' | 'inbox-zero'>(
|
||||
data.activityFeed.length === 0 ? 'first-run' : 'filter-empty'
|
||||
);
|
||||
|
||||
function retry() {
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.chronik_page_title()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="mb-6 flex items-baseline justify-between">
|
||||
<h1 class="font-serif text-2xl text-ink">{m.chronik_page_title()}</h1>
|
||||
</header>
|
||||
|
||||
{#if data.loadError === 'activity'}
|
||||
<ChronikErrorCard onRetry={retry} />
|
||||
{:else}
|
||||
<ChronikFuerDichBox unread={unread} onMarkRead={onMarkRead} onMarkAllRead={onMarkAllRead} />
|
||||
|
||||
<div class="mt-6">
|
||||
<ChronikFilterPills value={data.filter} onChange={onFilterChange} />
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" aria-atomic="false" aria-busy={!!navigating.type}>
|
||||
{#if isEmpty}
|
||||
<div class="mt-8">
|
||||
<ChronikEmptyState variant={emptyVariant} />
|
||||
</div>
|
||||
{:else}
|
||||
<ChronikTimeline items={displayFeed} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
Reference in New Issue
Block a user