From 86d31c97f5e30a6801826c7d4eabbe6238d05e9a Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 16:49:10 +0200 Subject: [PATCH] feat(manacore/web): expand contact detail page with all fields Add quick actions (call, email, SMS), work section (company, position, website), address (street/city/postal/country), social media (LinkedIn, Twitter, Instagram, GitHub), and mobile phone to both view and edit modes. Edit form organized in card sections matching the view layout. Extend Contact type and store with all new fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/modules/contacts/queries.ts | 10 + .../contacts/stores/contacts.svelte.ts | 13 +- .../web/src/lib/modules/contacts/types.ts | 10 + .../routes/(app)/contacts/[id]/+page.svelte | 487 ++++++++++++++---- 4 files changed, 423 insertions(+), 97 deletions(-) diff --git a/apps/manacore/apps/web/src/lib/modules/contacts/queries.ts b/apps/manacore/apps/web/src/lib/modules/contacts/queries.ts index c1b5c3922..fc6f80dea 100644 --- a/apps/manacore/apps/web/src/lib/modules/contacts/queries.ts +++ b/apps/manacore/apps/web/src/lib/modules/contacts/queries.ts @@ -21,11 +21,21 @@ export function toContact(local: LocalContact): Contact { displayName, email: local.email || null, phone: local.phone || null, + mobile: local.mobile || null, company: local.company || null, jobTitle: local.jobTitle || null, + street: local.street || null, + city: local.city || null, + postalCode: local.postalCode || null, + country: local.country || null, notes: local.notes || null, photoUrl: local.photoUrl || null, birthday: local.birthday || null, + website: local.website || null, + linkedin: local.linkedin || null, + twitter: local.twitter || null, + instagram: local.instagram || null, + github: local.github || null, tags: (local.tags || []).map((name, i) => ({ id: `tag-${i}`, name, color: null })), tagIds: local.tagIds ?? [], isFavorite: local.isFavorite ?? false, diff --git a/apps/manacore/apps/web/src/lib/modules/contacts/stores/contacts.svelte.ts b/apps/manacore/apps/web/src/lib/modules/contacts/stores/contacts.svelte.ts index 3699f7b50..22a92e15b 100644 --- a/apps/manacore/apps/web/src/lib/modules/contacts/stores/contacts.svelte.ts +++ b/apps/manacore/apps/web/src/lib/modules/contacts/stores/contacts.svelte.ts @@ -45,17 +45,28 @@ export const contactsStore = { return toContact(newLocal); }, - async updateContact(id: string, data: Partial) { + async updateContact(id: string, data: Partial & Record) { const updateData: Partial = {}; if (data.firstName !== undefined) updateData.firstName = data.firstName ?? undefined; if (data.lastName !== undefined) updateData.lastName = data.lastName ?? undefined; if (data.email !== undefined) updateData.email = data.email ?? undefined; if (data.phone !== undefined) updateData.phone = data.phone ?? undefined; + if (data.mobile !== undefined) updateData.mobile = data.mobile as string | undefined; if (data.company !== undefined) updateData.company = data.company ?? undefined; if (data.jobTitle !== undefined) updateData.jobTitle = data.jobTitle ?? undefined; + if (data.street !== undefined) updateData.street = data.street as string | undefined; + if (data.city !== undefined) updateData.city = data.city as string | undefined; + if (data.postalCode !== undefined) + updateData.postalCode = data.postalCode as string | undefined; + if (data.country !== undefined) updateData.country = data.country as string | undefined; if (data.notes !== undefined) updateData.notes = data.notes ?? undefined; if (data.photoUrl !== undefined) updateData.photoUrl = data.photoUrl ?? undefined; if (data.birthday !== undefined) updateData.birthday = data.birthday ?? undefined; + if (data.website !== undefined) updateData.website = data.website as string | undefined; + if (data.linkedin !== undefined) updateData.linkedin = data.linkedin as string | undefined; + if (data.twitter !== undefined) updateData.twitter = data.twitter as string | undefined; + if (data.instagram !== undefined) updateData.instagram = data.instagram as string | undefined; + if (data.github !== undefined) updateData.github = data.github as string | undefined; if (data.tags !== undefined) updateData.tags = data.tags?.map((t) => t.name) ?? []; if (data.isFavorite !== undefined) updateData.isFavorite = data.isFavorite; if (data.isArchived !== undefined) updateData.isArchived = data.isArchived; diff --git a/apps/manacore/apps/web/src/lib/modules/contacts/types.ts b/apps/manacore/apps/web/src/lib/modules/contacts/types.ts index 249db579b..48aeb2580 100644 --- a/apps/manacore/apps/web/src/lib/modules/contacts/types.ts +++ b/apps/manacore/apps/web/src/lib/modules/contacts/types.ts @@ -41,11 +41,21 @@ export interface Contact { displayName?: string | null; email?: string | null; phone?: string | null; + mobile?: string | null; company?: string | null; jobTitle?: string | null; + street?: string | null; + city?: string | null; + postalCode?: string | null; + country?: string | null; notes?: string | null; photoUrl?: string | null; birthday?: string | null; + website?: string | null; + linkedin?: string | null; + twitter?: string | null; + instagram?: string | null; + github?: string | null; tags: Array<{ id: string; name: string; color: string | null }>; tagIds: string[]; isFavorite: boolean; diff --git a/apps/manacore/apps/web/src/routes/(app)/contacts/[id]/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/contacts/[id]/+page.svelte index 70796aa85..fe9e1650f 100644 --- a/apps/manacore/apps/web/src/routes/(app)/contacts/[id]/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/contacts/[id]/+page.svelte @@ -12,10 +12,17 @@ PencilSimple, Envelope, Phone, + DeviceMobile, Buildings, + Briefcase, MapPin, Cake, Note, + Globe, + GithubLogo, + LinkedinLogo, + TwitterLogo, + InstagramLogo, ShareNetwork, } from '@manacore/shared-icons'; import { ShareModal } from '@manacore/shared-uload'; @@ -44,10 +51,20 @@ lastName: contact.lastName, email: contact.email, phone: contact.phone, + mobile: contact.mobile, company: contact.company, jobTitle: contact.jobTitle, + street: contact.street, + city: contact.city, + postalCode: contact.postalCode, + country: contact.country, notes: contact.notes, birthday: contact.birthday, + website: contact.website, + linkedin: contact.linkedin, + twitter: contact.twitter, + instagram: contact.instagram, + github: contact.github, }; isEditing = true; } @@ -89,6 +106,9 @@ : '' ); let shareTitle = $derived(contact ? getDisplayName(contact) : ''); + + const inputClass = + 'w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20'; @@ -200,138 +220,353 @@ {#if isEditing} -
-

- Bearbeiten -

-
+
+ +
+

+ Name +

(editData.firstName = e.currentTarget.value || null)} - class="rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" + class={inputClass} /> (editData.lastName = e.currentTarget.value || null)} - class="rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" + class={inputClass} />
- (editData.email = e.currentTarget.value || null)} - class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" - /> - (editData.phone = e.currentTarget.value || null)} - class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" - /> - (editData.company = e.currentTarget.value || null)} - class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" - /> - (editData.jobTitle = e.currentTarget.value || null)} - class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" - /> +
+ + +
+

+ Kontakt +

+
+ (editData.email = e.currentTarget.value || null)} + class={inputClass} + /> +
+ (editData.mobile = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.phone = e.currentTarget.value || null)} + class={inputClass} + /> +
+
+
+ + +
+

+ Arbeit +

+
+ (editData.company = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.jobTitle = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.website = e.currentTarget.value || null)} + class={inputClass} + /> +
+
+ + +
+

+ Adresse +

+
+ (editData.street = e.currentTarget.value || null)} + class={inputClass} + /> +
+ (editData.postalCode = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.city = e.currentTarget.value || null)} + class={inputClass} + /> +
+ (editData.country = e.currentTarget.value || null)} + class={inputClass} + /> +
+
+ + +
+

+ Geburtstag +

(editData.birthday = e.currentTarget.value || null)} - class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20" + class={inputClass} /> +
+ + +
+

+ Notizen +

+
-
- - + +
+

+ Social Media +

+
+ (editData.linkedin = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.twitter = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.instagram = e.currentTarget.value || null)} + class={inputClass} + /> + (editData.github = e.currentTarget.value || null)} + class={inputClass} + />
+ + +
+ + +
{:else} + + {#if contact.email || contact.phone || contact.mobile} +
+ {#if contact.phone} + + Anrufen + + {/if} + {#if contact.email} + + E-Mail + + {/if} + {#if contact.mobile} + + SMS + + {/if} +
+ {/if} +

- Kontaktdaten + Kontakt

{#if contact.email} + {/if} + {#if contact.mobile} +
+ + {contact.mobile} + Mobil
{/if} {#if contact.phone} - {/if} - {#if contact.company} -
- - {contact.company} -
- {/if} - {#if contact.birthday} -
- - - {new Date(contact.birthday).toLocaleDateString('de-DE', { - day: 'numeric', - month: 'long', - year: 'numeric', - })} - + {contact.phone} + Telefon
{/if}
- - {#if !contact.email && !contact.phone && !contact.company && !contact.birthday} + {#if !contact.email && !contact.phone && !contact.mobile}

Keine Kontaktdaten hinterlegt.

{/if}
+ + {#if contact.company || contact.jobTitle || contact.website} +
+

+ Arbeit +

+
+ {#if contact.company} +
+ + {contact.company} +
+ {/if} + {#if contact.jobTitle} +
+ + {contact.jobTitle} +
+ {/if} + {#if contact.website} + + {/if} +
+
+ {/if} + + + {#if contact.street || contact.city || contact.postalCode || contact.country} +
+

+ Adresse +

+
+ +
+ {#if contact.street}
{contact.street}
{/if} + {#if contact.postalCode || contact.city} +
{[contact.postalCode, contact.city].filter(Boolean).join(' ')}
+ {/if} + {#if contact.country}
{contact.country}
{/if} +
+
+
+ {/if} + + + {#if contact.birthday} +
+
+ + + {new Date(contact.birthday).toLocaleDateString('de-DE', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + +
+
+ {/if} + {#if contact.notes}
@@ -342,6 +577,69 @@
{/if} + + {#if contact.linkedin || contact.twitter || contact.instagram || contact.github} +
+

+ Social Media +

+
+ {#if contact.linkedin} + + {/if} + {#if contact.twitter} + + {/if} + {#if contact.instagram} + + {/if} + {#if contact.github} + + {/if} +
+
+ {/if} + {#if contact.tags.length > 0}
@@ -350,9 +648,9 @@
{#each contact.tags as tag (tag.id)} - - {tag.name} - + {tag.name} {/each}
@@ -360,26 +658,23 @@
-

- Details -

Erstellt - - {new Date(contact.createdAt).toLocaleDateString('de-DE', { + {new Date(contact.createdAt).toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric', - })} - + })} Aktualisiert - - {new Date(contact.updatedAt).toLocaleDateString('de-DE', { + {new Date(contact.updatedAt).toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric', - })} - + })}