refactor(contacts,todo): extract shared utilities, eliminate duplication

Contacts:
- Extract getDisplayName() + getInitials() to lib/utils/contact-display.ts
  (was duplicated across 7 files)
- Export UNKNOWN_CONTACT_NAME constant

Todo:
- Extract getSubtaskProgress() to lib/utils/task-helpers.ts
  (was duplicated in TaskItem + KanbanTaskCard)
- Add formatDateForInput() + dateInputToISO() to date-display.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 13:10:55 +02:00
parent 52e09e4ac0
commit bc1788941f
12 changed files with 67 additions and 106 deletions

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { contactsApi, photoApi, type Contact } from '$lib/api/contacts';
import { getDisplayName } from '$lib/utils/contact-display';
import ContactNotes from './ContactNotes.svelte';
import ContactTasks from './ContactTasks.svelte';
import { ContactDetailSkeleton } from '$lib/components/skeletons';
@ -128,15 +129,6 @@
bluesky = contact.bluesky || '';
}
function getDisplayName() {
if (!contact) return '';
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
async function loadContact() {
loading = true;
error = null;
@ -551,7 +543,7 @@
{#if contact.photoUrl}
<img
src={contact.photoUrl}
alt={getDisplayName()}
alt={getDisplayName(contact)}
class="avatar-image avatar-large"
/>
<button
@ -629,7 +621,7 @@
{/if}
</button>
</div>
<h2 class="profile-name">{getDisplayName()}</h2>
<h2 class="profile-name">{getDisplayName(contact)}</h2>
{#if contact.company || contact.jobTitle}
<p class="profile-subtitle">
{[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')}

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { contactsApi, type Contact } from '$lib/api/contacts';
import { getDisplayName, getInitials } from '$lib/utils/contact-display';
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
import { ContactsEvents } from '@manacore/shared-utils/analytics';
import { MagnifyingGlass, Heart, Plus, Tag, Upload } from '@manacore/shared-icons';
@ -90,20 +91,6 @@
onClose();
}
function getDisplayName(contact: Contact) {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
function getInitials(contact: Contact) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import type { Contact } from '$lib/api/contacts';
import { getDisplayName, getInitials } from '$lib/utils/contact-display';
interface Props {
isOpen: boolean;
@ -24,20 +25,6 @@
}
});
function getInitials(contact: Contact) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function getDisplayName(contact: Contact) {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
function getMatchTypeLabel(type: 'email' | 'phone' | 'name') {
switch (type) {
case 'email':

View file

@ -2,6 +2,7 @@
import { Plus, Check, Heart, Phone, Envelope, TextAa, CaretDown } from '@manacore/shared-icons';
import { _ } from 'svelte-i18n';
import type { Contact } from '$lib/api/contacts';
import { getDisplayName, getInitials } from '$lib/utils/contact-display';
import type { SortField } from '$lib/components/SortToggle.svelte';
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
import { contactsFilterStore } from '$lib/stores/filter.svelte';
@ -61,20 +62,6 @@
return letters;
});
function getInitials(contact: Contact) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function getDisplayName(contact: Contact) {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
function getFirstLetter(contact: Contact): string {
const name =
sortField === 'firstName'

View file

@ -2,6 +2,7 @@
import { Plus, Check, Heart, Phone, Envelope } from '@manacore/shared-icons';
import { _ } from 'svelte-i18n';
import type { Contact } from '$lib/api/contacts';
import { getDisplayName, getInitials } from '$lib/utils/contact-display';
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
@ -75,20 +76,6 @@
onToggleSelection?.(id);
}
function getInitials(contact: Contact) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function getDisplayName(contact: Contact) {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
// Generate a consistent gradient based on contact name
function getGradient(contact: Contact) {
const name = getDisplayName(contact);

View file

@ -0,0 +1,26 @@
import type { Contact } from '$lib/api/contacts';
export const UNKNOWN_CONTACT_NAME = 'Unbekannt';
export function getDisplayName(contact: {
displayName?: string | null;
firstName?: string | null;
lastName?: string | null;
email?: string | null;
}): string {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || UNKNOWN_CONTACT_NAME;
}
export function getInitials(contact: {
firstName?: string | null;
lastName?: string | null;
email?: string | null;
}): string {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}

View file

@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { contactsApi } from '$lib/api/contacts';
import type { Contact } from '$lib/api/contacts';
import { getDisplayName, getInitials } from '$lib/utils/contact-display';
import { ContactListSkeleton } from '$lib/components/skeletons';
import '$lib/i18n';
import {
@ -47,20 +48,6 @@
}
}
function getDisplayName(contact: Contact) {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
function getInitials(contact: Contact) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function handleContactClick(id: string) {
goto(`/contacts/${id}`);
}

View file

@ -6,6 +6,7 @@
import { DuplicateListSkeleton } from '$lib/components/skeletons';
import { toastStore } from '@manacore/shared-ui';
import { ArrowsClockwise } from '@manacore/shared-icons';
import { getDisplayName, getInitials } from '$lib/utils/contact-display';
let duplicates = $state<DuplicateGroup[]>([]);
let loading = $state(true);
@ -49,20 +50,6 @@
}
}
function getInitials(contact: DuplicateGroup['contacts'][0]) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function getDisplayName(contact: DuplicateGroup['contacts'][0]) {
if (contact.displayName) return contact.displayName;
if (contact.firstName || contact.lastName) {
return [contact.firstName, contact.lastName].filter(Boolean).join(' ');
}
return contact.email || 'Unbekannt';
}
function handleOpenMerge(group: DuplicateGroup) {
selectedGroup = group;
showMergeModal = true;