From 76d6f234b43a0e632ac81d14e6c02a7ddfee9c30 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 15 Apr 2026 13:07:23 +0200 Subject: [PATCH] refactor(personFormat): replace getInitials(Person) with getInitials(name: string) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unify the initials-extraction logic: the new string-based getInitials() splits on whitespace, takes the first char of the first and last word uppercased — matching the pattern that was already inlined in CommentThread. Update PersonChip, DocumentMetadataDrawer, and CommentThread to use the shared function. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/CommentThread.svelte | 9 +------ .../components/DocumentMetadataDrawer.svelte | 8 ++---- frontend/src/lib/components/PersonChip.svelte | 2 +- frontend/src/lib/utils/personFormat.spec.ts | 25 +++++++++++++++++++ frontend/src/lib/utils/personFormat.ts | 8 +++--- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/components/CommentThread.svelte b/frontend/src/lib/components/CommentThread.svelte index 6796cc3f..1d5ce5e5 100644 --- a/frontend/src/lib/components/CommentThread.svelte +++ b/frontend/src/lib/components/CommentThread.svelte @@ -5,6 +5,7 @@ import type { Comment } from '$lib/types'; import MentionEditor from '$lib/components/MentionEditor.svelte'; import { renderBody, extractContent } from '$lib/utils/mention'; import { relativeTime } from '$lib/utils/time'; +import { getInitials } from '$lib/utils/personFormat'; import type { MentionDTO } from '$lib/types'; type Props = { @@ -76,14 +77,6 @@ function isOwn(c: { authorId: string | null }): boolean { return currentUserId !== null && c.authorId === currentUserId; } -function getInitials(name: string): string { - return name - .split(/\s+/) - .slice(0, 2) - .map((w) => w.charAt(0).toUpperCase()) - .join(''); -} - function extractQuote(content: string): { quote: string | null; body: string } { const match = content.match(/^>\s*"(.+?)"\s*\n\n?([\s\S]*)$/); if (match) return { quote: match[1], body: match[2] }; diff --git a/frontend/src/lib/components/DocumentMetadataDrawer.svelte b/frontend/src/lib/components/DocumentMetadataDrawer.svelte index d057a993..a24a27d1 100644 --- a/frontend/src/lib/components/DocumentMetadataDrawer.svelte +++ b/frontend/src/lib/components/DocumentMetadataDrawer.svelte @@ -2,7 +2,7 @@ import { m } from '$lib/paraglide/messages.js'; import { formatDate } from '$lib/utils/date'; import { formatDocumentStatus } from '$lib/utils/documentStatusLabel'; -import { getInitials as calcInitials, personAvatarColor } from '$lib/utils/personFormat'; +import { getInitials, personAvatarColor } from '$lib/utils/personFormat'; type Person = { id: string; firstName?: string | null; lastName: string; displayName: string }; type Tag = { id: string; name: string }; @@ -32,10 +32,6 @@ let showAllReceivers = $state(false); const displayedReceivers = $derived(showAllReceivers ? receivers : visibleReceivers); -function getInitials(person: Person): string { - return calcInitials(person); -} - function getFullName(person: Person): string { return person.displayName; } @@ -51,7 +47,7 @@ function getFullName(person: Person): string { style="background-color: {personAvatarColor(person.id)}" aria-hidden="true" > - {getInitials(person)} + {getInitials(person.displayName)} {getFullName(person)} diff --git a/frontend/src/lib/components/PersonChip.svelte b/frontend/src/lib/components/PersonChip.svelte index 97796c00..89dd502a 100644 --- a/frontend/src/lib/components/PersonChip.svelte +++ b/frontend/src/lib/components/PersonChip.svelte @@ -12,7 +12,7 @@ let { person, abbreviated }: Props = $props(); const name = $derived(abbreviated ? abbreviateName(person) : person.displayName); const avatarColor = $derived(personAvatarColor(person.id)); -const initials = $derived(getInitials(person)); +const initials = $derived(getInitials(person.displayName)); { + it('returns first chars of first and last word uppercased', () => { + expect(getInitials('Marcel Raddatz')).toBe('MR'); + }); + + it('returns single char for a single-word name', () => { + expect(getInitials('Raddatz')).toBe('R'); + }); + + it('returns empty string for an empty name', () => { + expect(getInitials('')).toBe(''); + }); + + it('splits on whitespace only — hyphenated first word counts as one', () => { + expect(getInitials('Anna-Maria Raddatz')).toBe('AR'); + }); + + it('ignores extra whitespace between words', () => { + expect(getInitials(' Karl Raddatz ')).toBe('KR'); + }); +}); + // ─── abbreviateName ────────────────────────────────────────────────────────── describe('abbreviateName', () => { diff --git a/frontend/src/lib/utils/personFormat.ts b/frontend/src/lib/utils/personFormat.ts index 241339f3..d60023e0 100644 --- a/frontend/src/lib/utils/personFormat.ts +++ b/frontend/src/lib/utils/personFormat.ts @@ -18,9 +18,11 @@ function djb2(str: string): number { return Math.abs(hash); } -export function getInitials(person: Person): string { - if (person.firstName) return `${person.firstName[0]}${person.lastName[0]}`.toUpperCase(); - return person.lastName.substring(0, 2).toUpperCase(); +export 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(); } export function abbreviateName(person: Person): string {