Adds 5 new components for the permission-gated reader layout: - ReaderStatsStrip: stat tiles (documents / persons / stories) linking to list pages - ReaderPersonChips: top-N persons by doc count with avatar + name - ReaderDraftsModule: blog draft list for BLOG_WRITE users - ReaderRecentDocs: 5 most-recently-updated docs with Neu/Aktualisiert badge - ReaderRecentStories: 3 latest published stories with 150-char HTML-stripped excerpt Each component ships with a vitest-browser spec covering the key assertions. Avatar color/initials logic is inlined to satisfy $lib/shared → $lib/person boundary rule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
57 lines
2.0 KiB
Svelte
57 lines
2.0 KiB
Svelte
<script lang="ts">
|
|
import type { components } from '$lib/generated/api';
|
|
import * as m from '$lib/paraglide/messages.js';
|
|
|
|
const AVATAR_PALETTE = ['#012851', '#5A3080', '#007596', '#2A6040', '#803020'] as const;
|
|
function djb2(str: string): number {
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) hash = (hash * 33) ^ str.charCodeAt(i);
|
|
return Math.abs(hash);
|
|
}
|
|
function personAvatarColor(id: string): string {
|
|
return AVATAR_PALETTE[djb2(id) % AVATAR_PALETTE.length];
|
|
}
|
|
function getInitials(name: string): string {
|
|
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
if (words.length === 0) return '';
|
|
if (words.length === 1) return words[0].charAt(0).toUpperCase();
|
|
return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase();
|
|
}
|
|
|
|
type PersonSummaryDTO = components['schemas']['PersonSummaryDTO'];
|
|
|
|
interface Props {
|
|
persons: PersonSummaryDTO[];
|
|
}
|
|
|
|
const { persons }: Props = $props();
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
<h2 class="text-xs font-bold tracking-widest text-ink-3 uppercase">
|
|
{m.dashboard_reader_person_chips_heading()}
|
|
</h2>
|
|
<div class="flex flex-wrap gap-2">
|
|
{#each persons as p (p.id)}
|
|
<a
|
|
href="/persons/{p.id}"
|
|
class="flex min-h-[44px] items-center gap-2 rounded-sm border border-line bg-surface px-3 py-2 shadow-sm transition-colors hover:border-brand-mint focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none"
|
|
>
|
|
<span
|
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white"
|
|
style="background-color: {personAvatarColor(p.id ?? '')}"
|
|
>
|
|
{getInitials(p.displayName ?? p.lastName ?? '')}
|
|
</span>
|
|
<span class="flex min-w-0 flex-col">
|
|
<span class="text-ink-1 truncate font-serif text-sm">{p.displayName ?? p.lastName}</span>
|
|
<span class="font-sans text-xs text-ink-3">{p.documentCount ?? 0} Dok.</span>
|
|
</span>
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
<a href="/persons" class="self-end font-sans text-sm text-brand-mint hover:underline"
|
|
>{m.dashboard_reader_all_persons()}</a
|
|
>
|
|
</div>
|