mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
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:
parent
52e09e4ac0
commit
bc1788941f
12 changed files with 67 additions and 106 deletions
|
|
@ -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(' @ ')}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
26
apps/contacts/apps/web/src/lib/utils/contact-display.ts
Normal file
26
apps/contacts/apps/web/src/lib/utils/contact-display.ts
Normal 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() || '?';
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
} from '@todo/shared';
|
||||
import type { ContactReference, ContactOrManual } from '@manacore/shared-types';
|
||||
import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared';
|
||||
import { isToday, isPast } from 'date-fns';
|
||||
import { format, isToday, isPast } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { formatDueDate } from '$lib/utils/date-display';
|
||||
import { getSubtaskProgress } from '$lib/utils/task-helpers';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects, getProjectColor } from '$lib/data/task-queries';
|
||||
|
|
@ -314,11 +316,7 @@
|
|||
});
|
||||
|
||||
// Subtasks progress
|
||||
let subtaskProgress = $derived(() => {
|
||||
if (!task.subtasks || task.subtasks.length === 0) return null;
|
||||
const completed = task.subtasks.filter((s) => s.isCompleted).length;
|
||||
return `${completed}/${task.subtasks.length}`;
|
||||
});
|
||||
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks));
|
||||
|
||||
// Long press to expand (mobile)
|
||||
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import type { Task } from '@todo/shared';
|
||||
import { isToday, isPast } from 'date-fns';
|
||||
import { formatDueDate } from '$lib/utils/date-display';
|
||||
import { getSubtaskProgress } from '$lib/utils/task-helpers';
|
||||
import { ConfirmationModal, ContactAvatar } from '@manacore/shared-ui';
|
||||
import TaskEditModal from '../TaskEditModal.svelte';
|
||||
import {
|
||||
|
|
@ -53,11 +54,7 @@
|
|||
});
|
||||
|
||||
// Subtasks progress
|
||||
let subtaskProgress = $derived(() => {
|
||||
if (!task.subtasks || task.subtasks.length === 0) return null;
|
||||
const completed = task.subtasks.filter((s) => s.isCompleted).length;
|
||||
return `${completed}/${task.subtasks.length}`;
|
||||
});
|
||||
let subtaskProgress = $derived(() => getSubtaskProgress(task.subtasks));
|
||||
|
||||
// Click to open modal
|
||||
function handleCardClick(e: MouseEvent) {
|
||||
|
|
|
|||
|
|
@ -10,3 +10,20 @@ export function formatDueDate(date: Date): string {
|
|||
if (isTomorrow(date)) return 'Morgen';
|
||||
return format(date, 'dd. MMM', { locale: de });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date string for use in an HTML date input (yyyy-MM-dd).
|
||||
* Returns empty string if no date is provided.
|
||||
*/
|
||||
export function formatDateForInput(dateString?: string | null): string {
|
||||
if (!dateString) return '';
|
||||
return format(new Date(dateString), 'yyyy-MM-dd');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an HTML date input value (yyyy-MM-dd) to an ISO string.
|
||||
* Returns null if the input is empty.
|
||||
*/
|
||||
export function dateInputToISO(dateString: string): string | null {
|
||||
return dateString ? new Date(dateString).toISOString() : null;
|
||||
}
|
||||
|
|
|
|||
9
apps/todo/apps/web/src/lib/utils/task-helpers.ts
Normal file
9
apps/todo/apps/web/src/lib/utils/task-helpers.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Calculate subtask progress as a string like "2/5".
|
||||
* Returns null if there are no subtasks.
|
||||
*/
|
||||
export function getSubtaskProgress(subtasks?: { isCompleted: boolean }[]): string | null {
|
||||
if (!subtasks || subtasks.length === 0) return null;
|
||||
const completed = subtasks.filter((s) => s.isCompleted).length;
|
||||
return `${completed}/${subtasks.length}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue