From 4e5d12aa53bdbcaa2a0b19f6de95d09d1bb370ac Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:00:55 +0100 Subject: [PATCH] feat(contacts): add enhanced favorites page with multiple view modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add quick favorites filter button on homepage with badge count - Create dedicated favorites page with hero header and stats cards - Implement three view modes for favorites: cards, list, alphabet - Cards: Large 120px avatars with gradient backgrounds, full contact details - List: 72px avatars with detail chips and hover actions - Alphabet: Grouped by letter with quick-jump navigation - Fix layout jump when favorites filter is active (exclude from FilterBar count) - Add tags management feature with CRUD operations - Reorganize import page to /data route - Add infinite scroll to contacts list - Add contact notes feature - Persist favorites view mode in localStorage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/contacts/CLAUDE.md | 2 +- .../apps/backend/src/tag/tag.controller.ts | 39 + .../contacts/apps/web/src/lib/api/contacts.ts | 24 +- .../lib/components/ContactDetailModal.svelte | 4 + .../web/src/lib/components/ContactList.svelte | 197 +++- .../src/lib/components/ContactNotes.svelte | 598 +++++++++++++ .../web/src/lib/components/FilterBar.svelte | 11 +- .../favorites/FavoriteAlphabetView.svelte | 494 ++++++++++ .../favorites/FavoriteCardView.svelte | 363 ++++++++ .../favorites/FavoriteListView.svelte | 324 +++++++ .../lib/components/import/GoogleImport.svelte | 2 +- .../apps/web/src/lib/i18n/locales/de.json | 34 +- .../apps/web/src/lib/i18n/locales/en.json | 34 +- .../web/src/lib/stores/contacts.svelte.ts | 52 +- .../apps/web/src/routes/(app)/+layout.svelte | 1 + .../web/src/routes/(app)/data/+page.svelte | 635 +++++++++++++ .../src/routes/(app)/favorites/+page.svelte | 768 ++++++++++------ .../web/src/routes/(app)/import/+page.svelte | 283 ------ .../src/routes/(app)/settings/+page.svelte | 57 +- .../web/src/routes/(app)/tags/+page.svelte | 847 ++++++++++++++++++ 20 files changed, 4127 insertions(+), 642 deletions(-) create mode 100644 apps/contacts/apps/web/src/lib/components/ContactNotes.svelte create mode 100644 apps/contacts/apps/web/src/lib/components/favorites/FavoriteAlphabetView.svelte create mode 100644 apps/contacts/apps/web/src/lib/components/favorites/FavoriteCardView.svelte create mode 100644 apps/contacts/apps/web/src/lib/components/favorites/FavoriteListView.svelte create mode 100644 apps/contacts/apps/web/src/routes/(app)/data/+page.svelte delete mode 100644 apps/contacts/apps/web/src/routes/(app)/import/+page.svelte create mode 100644 apps/contacts/apps/web/src/routes/(app)/tags/+page.svelte diff --git a/apps/contacts/CLAUDE.md b/apps/contacts/CLAUDE.md index a95ef9c01..fde22e3f9 100644 --- a/apps/contacts/CLAUDE.md +++ b/apps/contacts/CLAUDE.md @@ -208,7 +208,7 @@ S3_BUCKET=contacts-photos # Get credentials from https://console.cloud.google.com/apis/credentials GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret -GOOGLE_REDIRECT_URI=http://localhost:5184/import?tab=google +GOOGLE_REDIRECT_URI=http://localhost:5184/data?tab=import&source=google ``` #### Mobile (.env) diff --git a/apps/contacts/apps/backend/src/tag/tag.controller.ts b/apps/contacts/apps/backend/src/tag/tag.controller.ts index 0371afd80..d77db0efa 100644 --- a/apps/contacts/apps/backend/src/tag/tag.controller.ts +++ b/apps/contacts/apps/backend/src/tag/tag.controller.ts @@ -71,4 +71,43 @@ export class TagController { await this.tagService.delete(id, user.userId); return { success: true }; } + + @Post(':id/contacts/:contactId') + async addToContact( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) tagId: string, + @Param('contactId', ParseUUIDPipe) contactId: string + ) { + // Verify tag belongs to user + const tag = await this.tagService.findById(tagId, user.userId); + if (!tag) { + throw new Error('Tag not found'); + } + await this.tagService.addTagToContact(contactId, tagId); + return { success: true }; + } + + @Delete(':id/contacts/:contactId') + async removeFromContact( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) tagId: string, + @Param('contactId', ParseUUIDPipe) contactId: string + ) { + // Verify tag belongs to user + const tag = await this.tagService.findById(tagId, user.userId); + if (!tag) { + throw new Error('Tag not found'); + } + await this.tagService.removeTagFromContact(contactId, tagId); + return { success: true }; + } + + @Get('contact/:contactId') + async getTagsForContact( + @CurrentUser() user: CurrentUserData, + @Param('contactId', ParseUUIDPipe) contactId: string + ) { + const tagIds = await this.tagService.getTagsForContact(contactId); + return { tagIds }; + } } diff --git a/apps/contacts/apps/web/src/lib/api/contacts.ts b/apps/contacts/apps/web/src/lib/api/contacts.ts index 2288422f0..b7b6be705 100644 --- a/apps/contacts/apps/web/src/lib/api/contacts.ts +++ b/apps/contacts/apps/web/src/lib/api/contacts.ts @@ -210,29 +210,45 @@ export const groupsApi = { // Tags API export const tagsApi = { - async list() { + async list(): Promise<{ tags: ContactTag[] }> { return fetchWithAuth('/tags'); }, - async create(data: { name: string; color?: string }) { + async create(data: { name: string; color?: string }): Promise<{ tag: ContactTag }> { return fetchWithAuth('/tags', { method: 'POST', body: JSON.stringify(data), }); }, - async update(id: string, data: { name?: string; color?: string }) { + async update(id: string, data: { name?: string; color?: string }): Promise<{ tag: ContactTag }> { return fetchWithAuth(`/tags/${id}`, { method: 'PATCH', body: JSON.stringify(data), }); }, - async delete(id: string) { + async delete(id: string): Promise<{ success: boolean }> { return fetchWithAuth(`/tags/${id}`, { method: 'DELETE', }); }, + + async addToContact(tagId: string, contactId: string): Promise<{ success: boolean }> { + return fetchWithAuth(`/tags/${tagId}/contacts/${contactId}`, { + method: 'POST', + }); + }, + + async removeFromContact(tagId: string, contactId: string): Promise<{ success: boolean }> { + return fetchWithAuth(`/tags/${tagId}/contacts/${contactId}`, { + method: 'DELETE', + }); + }, + + async getForContact(contactId: string): Promise<{ tagIds: string[] }> { + return fetchWithAuth(`/tags/contact/${contactId}`); + }, }; // Notes API diff --git a/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte b/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte index 2dbdeae71..f140824e4 100644 --- a/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte +++ b/apps/contacts/apps/web/src/lib/components/ContactDetailModal.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import { contactsApi, photoApi, type Contact } from '$lib/api/contacts'; + import ContactNotes from './ContactNotes.svelte'; interface Props { contactId: string; @@ -848,6 +849,9 @@ {/if} + + + {/if} {/if} diff --git a/apps/contacts/apps/web/src/lib/components/ContactList.svelte b/apps/contacts/apps/web/src/lib/components/ContactList.svelte index 7783fb2fd..cebb714a7 100644 --- a/apps/contacts/apps/web/src/lib/components/ContactList.svelte +++ b/apps/contacts/apps/web/src/lib/components/ContactList.svelte @@ -1,10 +1,9 @@ @@ -248,22 +296,6 @@ - + {$_('contacts.new')} @@ -372,6 +404,32 @@ /> + + {/if} + + {#if contactsStore.hasMore} +
+ {#if contactsStore.loadingMore} +
+
+ {$_('common.loadingMore')} +
+ {/if} +
+ {/if} +

- {contactsStore.total} + {contactsStore.contacts.length} / {contactsStore.total} {contactsStore.total === 1 ? $_('contacts.contact') : $_('contacts.contactsPlural')}

{/if} - - (showExportModal = false)} /> - diff --git a/apps/contacts/apps/web/src/lib/components/ContactNotes.svelte b/apps/contacts/apps/web/src/lib/components/ContactNotes.svelte new file mode 100644 index 000000000..744a208f4 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/components/ContactNotes.svelte @@ -0,0 +1,598 @@ + + +
+
+
+ + + +
+

{$_('notes.title')}

+ +
+ + {#if error} +
{error}
+ {/if} + + + {#if showAddForm} +
+ +
+ + +
+
+ {/if} + + + {#if loading} +
+ +
+ {:else if notes.length === 0 && !showAddForm} +
+

{$_('notes.empty')}

+ +
+ {:else} +
+ {#each sortedNotes as note (note.id)} +
+ {#if editingNoteId === note.id} + + +
+ + +
+ {:else} + +
+ {#if note.isPinned} + + + + + + {/if} +

{note.content}

+ {formatDate(note.createdAt)} +
+
+ + + +
+ {/if} +
+ {/each} +
+ {/if} +
+ + diff --git a/apps/contacts/apps/web/src/lib/components/FilterBar.svelte b/apps/contacts/apps/web/src/lib/components/FilterBar.svelte index df6adce69..bbe3d546c 100644 --- a/apps/contacts/apps/web/src/lib/components/FilterBar.svelte +++ b/apps/contacts/apps/web/src/lib/components/FilterBar.svelte @@ -45,11 +45,11 @@ return Array.from(companySet).sort((a, b) => a.localeCompare(b, 'de')); }); - // Count active filters + // Count active filters (excluding favorites since it has its own quick button) let activeFilterCount = $derived.by(() => { let count = 0; if (selectedGroupId) count++; - if (contactFilter !== 'all') count++; + if (contactFilter !== 'all' && contactFilter !== 'favorites') count++; if (birthdayFilter !== 'all') count++; if (selectedCompany) count++; return count; @@ -68,7 +68,10 @@ function clearAllFilters() { onGroupChange(null); - onContactFilterChange('all'); + // Keep favorites filter if active (controlled by separate quick button) + if (contactFilter !== 'favorites') { + onContactFilterChange('all'); + } onBirthdayFilterChange('all'); onCompanyChange(null); } @@ -120,7 +123,7 @@ {/if} {/if} - {#if contactFilter !== 'all'} + {#if contactFilter !== 'all' && contactFilter !== 'favorites'}
+ + + {/each} + + + {/each} + + + +
+ {#each alphabet as letter} + + {/each} + {#if availableLetters.includes('#')} + + {/if} +
+ + + diff --git a/apps/contacts/apps/web/src/lib/components/favorites/FavoriteCardView.svelte b/apps/contacts/apps/web/src/lib/components/favorites/FavoriteCardView.svelte new file mode 100644 index 000000000..de131246c --- /dev/null +++ b/apps/contacts/apps/web/src/lib/components/favorites/FavoriteCardView.svelte @@ -0,0 +1,363 @@ + + +
+ {#each contacts as contact (contact.id)} +
onContactClick(contact.id)} + onkeydown={(e) => e.key === 'Enter' && onContactClick(contact.id)} + class="favorite-card" + > + +
+ + + + + +
+ {#if contact.photoUrl} + {getDisplayName(contact)} + {:else} + {getInitials(contact)} + {/if} +
+ + +
+

{getDisplayName(contact)}

+ {#if contact.jobTitle} +

{contact.jobTitle}

+ {/if} + {#if contact.company} +

{contact.company}

+ {/if} + + +
+ {#if contact.email} +
+ + + + {contact.email} +
+ {/if} + {#if contact.phone || contact.mobile} +
+ + + + {contact.mobile || contact.phone} +
+ {/if} + {#if contact.birthday} +
+ + + + {new Date(contact.birthday).toLocaleDateString('de-DE', { + day: 'numeric', + month: 'long', + })} +
+ {/if} +
+
+ + + +
+ {/each} +
+ + diff --git a/apps/contacts/apps/web/src/lib/components/favorites/FavoriteListView.svelte b/apps/contacts/apps/web/src/lib/components/favorites/FavoriteListView.svelte new file mode 100644 index 000000000..2377f9f93 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/components/favorites/FavoriteListView.svelte @@ -0,0 +1,324 @@ + + +
+ {#each contacts as contact (contact.id)} +
onContactClick(contact.id)} + onkeydown={(e) => e.key === 'Enter' && onContactClick(contact.id)} + class="favorite-row" + > + +
+ {#if contact.photoUrl} + {getDisplayName(contact)} + {:else} + {getInitials(contact)} + {/if} +
+ + +
+
+

{getDisplayName(contact)}

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

+ {[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')} +

+ {/if} +
+ + +
+ {#if contact.email} +
+ + + + {contact.email} +
+ {/if} + {#if contact.phone || contact.mobile} +
+ + + + {contact.mobile || contact.phone} +
+ {/if} + {#if contact.birthday} +
+ + + + {new Date(contact.birthday).toLocaleDateString('de-DE', { + day: 'numeric', + month: 'short', + })} +
+ {/if} +
+
+ + +
+ {#if contact.phone || contact.mobile} + e.stopPropagation()} + class="action-btn action-call" + title="Anrufen" + > + + + + + {/if} + {#if contact.email} + e.stopPropagation()} + class="action-btn action-email" + title="E-Mail senden" + > + + + + + {/if} + +
+
+ {/each} +
+ + diff --git a/apps/contacts/apps/web/src/lib/components/import/GoogleImport.svelte b/apps/contacts/apps/web/src/lib/components/import/GoogleImport.svelte index 47bd95bd7..258da86fc 100644 --- a/apps/contacts/apps/web/src/lib/components/import/GoogleImport.svelte +++ b/apps/contacts/apps/web/src/lib/components/import/GoogleImport.svelte @@ -31,7 +31,7 @@ try { await googleApi.handleCallback(code); // Remove code from URL - goto('/import?tab=google', { replaceState: true }); + goto('/data?tab=import&source=google', { replaceState: true }); } catch (e) { error = e instanceof Error ? e.message : 'Failed to connect'; } diff --git a/apps/contacts/apps/web/src/lib/i18n/locales/de.json b/apps/contacts/apps/web/src/lib/i18n/locales/de.json index 46a012ab2..bfeb20ee8 100644 --- a/apps/contacts/apps/web/src/lib/i18n/locales/de.json +++ b/apps/contacts/apps/web/src/lib/i18n/locales/de.json @@ -4,11 +4,13 @@ }, "common": { "back": "Zurück", - "cancel": "Abbrechen" + "cancel": "Abbrechen", + "loadingMore": "Lade weitere..." }, "nav": { "contacts": "Kontakte", "groups": "Gruppen", + "tags": "Tags", "favorites": "Favoriten", "archive": "Archiv", "search": "Suche", @@ -143,6 +145,7 @@ "contactInfo": "Kontaktinfo", "contact": { "all": "Alle Kontakte", + "favorites": "Favoriten", "hasPhone": "Mit Telefon", "hasEmail": "Mit E-Mail", "incomplete": "Unvollständig" @@ -166,5 +169,34 @@ "includeArchived": "Archivierte Kontakte einschließen", "exporting": "Exportiere...", "success": "Export erfolgreich" + }, + "notes": { + "title": "Notizen", + "add": "Notiz hinzufügen", + "addFirst": "Erste Notiz hinzufügen", + "empty": "Noch keine Notizen", + "placeholder": "Schreibe eine Notiz...", + "confirmDelete": "Diese Notiz löschen?", + "pin": "Notiz anheften", + "unpin": "Nicht mehr anheften", + "yesterday": "Gestern" + }, + "tags": { + "title": "Tags", + "new": "Neuer Tag", + "edit": "Tag bearbeiten", + "noTags": "Noch keine Tags", + "createFirst": "Erstelle deinen ersten Tag um Kontakte zu organisieren", + "search": "Tags durchsuchen...", + "name": "Name", + "namePlaceholder": "Tag-Name eingeben", + "color": "Farbe", + "preview": "Vorschau", + "contactCount": "{count} Kontakte", + "confirmDelete": "Möchtest du \"{name}\" wirklich löschen?", + "noResults": "Keine Tags gefunden", + "noResultsFor": "Keine Ergebnisse für \"{query}\"", + "tagSingular": "Tag", + "tagPlural": "Tags" } } diff --git a/apps/contacts/apps/web/src/lib/i18n/locales/en.json b/apps/contacts/apps/web/src/lib/i18n/locales/en.json index 29fe1c00c..cd1092c6e 100644 --- a/apps/contacts/apps/web/src/lib/i18n/locales/en.json +++ b/apps/contacts/apps/web/src/lib/i18n/locales/en.json @@ -4,11 +4,13 @@ }, "common": { "back": "Back", - "cancel": "Cancel" + "cancel": "Cancel", + "loadingMore": "Loading more..." }, "nav": { "contacts": "Contacts", "groups": "Groups", + "tags": "Tags", "favorites": "Favorites", "archive": "Archive", "search": "Search", @@ -143,6 +145,7 @@ "contactInfo": "Contact info", "contact": { "all": "All contacts", + "favorites": "Favorites", "hasPhone": "With phone", "hasEmail": "With email", "incomplete": "Incomplete" @@ -166,5 +169,34 @@ "includeArchived": "Include archived contacts", "exporting": "Exporting...", "success": "Export successful" + }, + "notes": { + "title": "Notes", + "add": "Add Note", + "addFirst": "Add your first note", + "empty": "No notes yet", + "placeholder": "Write a note...", + "confirmDelete": "Delete this note?", + "pin": "Pin note", + "unpin": "Unpin note", + "yesterday": "Yesterday" + }, + "tags": { + "title": "Tags", + "new": "New Tag", + "edit": "Edit Tag", + "noTags": "No tags yet", + "createFirst": "Create your first tag to organize contacts", + "search": "Search tags...", + "name": "Name", + "namePlaceholder": "Enter tag name", + "color": "Color", + "preview": "Preview", + "contactCount": "{count} contacts", + "confirmDelete": "Are you sure you want to delete \"{name}\"?", + "noResults": "No tags found", + "noResultsFor": "No results for \"{query}\"", + "tagSingular": "Tag", + "tagPlural": "Tags" } } diff --git a/apps/contacts/apps/web/src/lib/stores/contacts.svelte.ts b/apps/contacts/apps/web/src/lib/stores/contacts.svelte.ts index 7f9af9df8..758d1c0e8 100644 --- a/apps/contacts/apps/web/src/lib/stores/contacts.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/contacts.svelte.ts @@ -5,13 +5,19 @@ import { contactsApi } from '$lib/api/contacts'; import type { Contact, ContactFilters } from '$lib/api/contacts'; +// Default page size for pagination +const DEFAULT_PAGE_SIZE = 50; + // State let contacts = $state([]); let selectedContact = $state(null); let loading = $state(false); +let loadingMore = $state(false); let error = $state(null); let total = $state(0); let filters = $state({}); +let hasMore = $state(true); +let currentOffset = $state(0); export const contactsStore = { // Getters @@ -24,6 +30,9 @@ export const contactsStore = { get loading() { return loading; }, + get loadingMore() { + return loadingMore; + }, get error() { return error; }, @@ -33,9 +42,12 @@ export const contactsStore = { get filters() { return filters; }, + get hasMore() { + return hasMore; + }, /** - * Load contacts with optional filters + * Load contacts with optional filters (resets to first page) */ async loadContacts(newFilters?: ContactFilters) { if (newFilters) { @@ -44,11 +56,18 @@ export const contactsStore = { loading = true; error = null; + currentOffset = 0; try { - const result = await contactsApi.list(filters); + const result = await contactsApi.list({ + ...filters, + limit: DEFAULT_PAGE_SIZE, + offset: 0, + }); contacts = result.contacts; total = result.total; + hasMore = contacts.length < total; + currentOffset = contacts.length; } catch (e) { error = e instanceof Error ? e.message : 'Failed to load contacts'; console.error('Failed to load contacts:', e); @@ -57,6 +76,35 @@ export const contactsStore = { } }, + /** + * Load more contacts (infinite scroll) + */ + async loadMore() { + if (loadingMore || !hasMore) return; + + loadingMore = true; + error = null; + + try { + const result = await contactsApi.list({ + ...filters, + limit: DEFAULT_PAGE_SIZE, + offset: currentOffset, + }); + + const newContacts = result.contacts; + contacts = [...contacts, ...newContacts]; + total = result.total; + currentOffset += newContacts.length; + hasMore = contacts.length < total; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to load more contacts'; + console.error('Failed to load more contacts:', e); + } finally { + loadingMore = false; + } + }, + /** * Load a single contact by ID */ diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index c4b06dd68..9b4543436 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -77,6 +77,7 @@ const navItems: PillNavItem[] = [ { href: '/', label: 'Kontakte', icon: 'users' }, { href: '/groups', label: 'Gruppen', icon: 'folder' }, + { href: '/tags', label: 'Tags', icon: 'tag' }, { href: '/favorites', label: 'Favoriten', icon: 'heart' }, { href: '/archive', label: 'Archiv', icon: 'archive' }, { href: '/settings', label: 'Einstellungen', icon: 'settings' }, diff --git a/apps/contacts/apps/web/src/routes/(app)/data/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/data/+page.svelte new file mode 100644 index 000000000..319da69bb --- /dev/null +++ b/apps/contacts/apps/web/src/routes/(app)/data/+page.svelte @@ -0,0 +1,635 @@ + + + + Daten - Kontakte + + +
+ +
+
+

Daten verwalten

+

Kontakte importieren, exportieren und sichern

+
+ Zurück +
+ + +
+ + +
+ + + {#if activeTab === 'import'} + +
+ + +
+ + + {#if importError && importSource === 'file'} +
+ {importError} +
+ {/if} + + + {#if importSource === 'file'} + {#if importStep === 'upload'} +
+ {#if isLoading} +
+
+

Datei wird verarbeitet...

+
+ {:else} + + +
+

Unterstützte Formate

+
+
+
+ + + +
+
+
vCard (.vcf)
+
+ Standard-Format für Kontakte, kompatibel mit allen gängigen Apps +
+
+
+
+
+ + + +
+
+
CSV (.csv)
+
+ Tabellen-Format, ideal für Excel oder Google Sheets +
+
+
+
+ +
+ +
+
+ {/if} +
+ {/if} + + {#if importStep === 'preview' && preview} + + {/if} + + {#if importStep === 'result' && importResult} +
+
+ + + +
+ +
+

Import abgeschlossen

+

Deine Kontakte wurden erfolgreich importiert

+
+ +
+
+
{importResult.imported}
+
Importiert
+
+
+
{importResult.merged}
+
Zusammengeführt
+
+
+
{importResult.skipped}
+
Übersprungen
+
+
+ + {#if importResult.errors.length > 0} +
+

Fehler

+
    + {#each importResult.errors as err} +
  • {err.contactName}: {err.error}
  • + {/each} +
+
+ {/if} + +
+ + +
+
+ {/if} + {/if} + + + {#if importSource === 'google'} + + {/if} + {/if} + + + {#if activeTab === 'export'} +
+ + {#if exportSuccess} +
+ + + + Export erfolgreich! Die Datei wurde heruntergeladen. +
+ {/if} + + + {#if exportError} +
+ {exportError} +
+ {/if} + + +
+

Format wählen

+
+ + +
+
+ + +
+

Filter

+ +
+ +
+ + +
+ + + +
+
+ + +
+

Optionen

+ +
+ + + + + +
+
+ + +
+ +
+
+ {/if} +
diff --git a/apps/contacts/apps/web/src/routes/(app)/favorites/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/favorites/+page.svelte index 92ddfcfa7..3bf52e8d1 100644 --- a/apps/contacts/apps/web/src/routes/(app)/favorites/+page.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/favorites/+page.svelte @@ -1,16 +1,23 @@ Favoriten - Contacts -
- -
- - - - - -

Favoriten

-
- +
+ +
+
+
+ + + +
+
+

Favoriten

+

+ {#if contacts.length === 0} + Markiere Kontakte als Favoriten für schnellen Zugriff + {:else} + {contacts.length} Favorit{contacts.length !== 1 ? 'en' : ''} für schnellen Zugriff + {/if} +

+
+
+ + + {#if contacts.length > 0} +
+
+
+ + + +
+
+ {contacts.length} + Favoriten +
+
+
+
+ + + +
+
+ {contacts.filter((c) => c.email).length} + Mit E-Mail +
+
+
+
+ + + +
+
+ {contacts.filter((c) => c.phone || c.mobile).length} + Mit Telefon +
+
+
+ {/if} +
+ + +
+ +
+ -
-
- - -
- - - - + {#if searchQuery} + + {/if} +
+ + +
+ + + +
{#if error} {:else if contacts.length === 0}
@@ -134,17 +282,18 @@

Keine Favoriten

- Markiere Kontakte als Favoriten, um sie hier schnell zu finden. + Markiere Kontakte als Favoriten, um sie hier schnell wiederzufinden. Klicke einfach auf das + Herz-Symbol bei einem Kontakt.

- - + + - Zu Kontakten + Zu allen Kontakten - {:else if filteredContacts().length === 0} + {:else if filteredContacts.length === 0}
-
+

Keine Ergebnisse

Keine Favoriten gefunden für "{searchQuery}"

+
{:else} -
- {#each filteredContacts() as contact (contact.id)} -
handleContactClick(contact.id)} - onkeydown={(e) => e.key === 'Enter' && handleContactClick(contact.id)} - class="contact-card" - > - -
- {#if contact.photoUrl} - {getDisplayName(contact)} - {:else} - {getInitials(contact)} - {/if} -
- - -
-

{getDisplayName(contact)}

- {#if contact.company || contact.jobTitle} -

- {[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')} -

- {/if} - {#if contact.email} -

{contact.email}

- {/if} -
- - - -
- {/each} + +
+ {#if viewMode === 'cards'} + + {:else if viewMode === 'list'} + + {:else} + + {/if}
-

{contacts.length} Favorit{contacts.length !== 1 ? 'en' : ''}

+ + {/if}
diff --git a/apps/contacts/apps/web/src/routes/(app)/import/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/import/+page.svelte deleted file mode 100644 index 3fb8a9128..000000000 --- a/apps/contacts/apps/web/src/routes/(app)/import/+page.svelte +++ /dev/null @@ -1,283 +0,0 @@ - - - - {$_('import.title')} - Contacts - - -
- - - - -
- - -
- - - {#if error && activeTab === 'file'} -
- {error} -
- {/if} - - - {#if activeTab === 'file'} - - {#if step === 'upload'} -
- {#if isLoading} -
-
-

{$_('import.processing')}

-
- {:else} - - -
- -
- {/if} -
- {/if} - - - {#if step === 'preview' && preview} - - {/if} - - - {#if step === 'result' && result} -
-
- - - -
- -
-

{$_('import.result.title')}

-

{$_('import.result.subtitle')}

-
- -
-
-
{result.imported}
-
{$_('import.result.imported')}
-
-
-
{result.merged}
-
{$_('import.result.merged')}
-
-
-
{result.skipped}
-
{$_('import.result.skipped')}
-
-
- - {#if result.errors.length > 0} -
-

{$_('import.result.errors')}

-
    - {#each result.errors as err} -
  • {err.contactName}: {err.error}
  • - {/each} -
-
- {/if} - -
- - -
-
- {/if} - {/if} - - - {#if activeTab === 'google'} - - {/if} -
diff --git a/apps/contacts/apps/web/src/routes/(app)/settings/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/settings/+page.svelte index 2db88da06..ab38cd902 100644 --- a/apps/contacts/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/settings/+page.svelte @@ -53,12 +53,6 @@ { value: 'yyyy-MM-dd', label: 'JJJJ-MM-TT (ISO)' }, ]; - const exportFormatOptions = [ - { value: 'vcf', label: 'vCard (.vcf)' }, - { value: 'csv', label: 'CSV (.csv)' }, - { value: 'json', label: 'JSON (.json)' }, - ]; - const duplicateSensitivityOptions = [ { value: 'strict', label: 'Streng' }, { value: 'normal', label: 'Normal' }, @@ -195,8 +189,7 @@ description="Standard-Sortierung der Kontakte" options={sortByOptions} value={contactsSettings.sortBy} - onchange={(v: string | number | null) => - contactsSettings.set('sortBy', v as ContactSortBy)} + onchange={(v: string | number | null) => contactsSettings.set('sortBy', v as ContactSortBy)} > {#snippet icon()} @@ -385,7 +378,7 @@ - + {#snippet icon()} - - contactsSettings.set('defaultExportFormat', v as 'vcf' | 'csv' | 'json')} + {#snippet icon()} @@ -412,35 +402,16 @@ stroke-linecap="round" stroke-linejoin="round" stroke-width="2" - d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /> {/snippet} - + - contactsSettings.set('includeNotesInExport', v)} - > - {#snippet icon()} - - - - {/snippet} - - - contactsSettings.set('includePhotosInExport', v)} + {#snippet icon()} @@ -449,11 +420,11 @@ stroke-linecap="round" stroke-linejoin="round" stroke-width="2" - d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" + d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> {/snippet} - + diff --git a/apps/contacts/apps/web/src/routes/(app)/tags/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/tags/+page.svelte new file mode 100644 index 000000000..e01ec9d85 --- /dev/null +++ b/apps/contacts/apps/web/src/routes/(app)/tags/+page.svelte @@ -0,0 +1,847 @@ + + + + {$_('tags.title')} - Contacts + + +
+ +
+ + + + + +

{$_('tags.title')}

+ +
+ + +
+ + + + +
+ + {#if error} + + {/if} + + {#if loading} +
+
+
+ {:else if tags.length === 0} +
+
+ + + +
+

{$_('tags.noTags')}

+

{$_('tags.createFirst')}

+ +
+ {:else if filteredTags.length === 0} +
+
+ + + +
+

{$_('tags.noResults')}

+

{$_('tags.noResultsFor', { values: { query: searchQuery } })}

+
+ {:else} +
+ {#each filteredTags as tag (tag.id)} +
+
+ + + +
+
+

{tag.name}

+
+
+ + +
+
+ {/each} +
+ +

+ {tags.length} + {tags.length === 1 ? $_('tags.tagSingular') : $_('tags.tagPlural')} +

+ {/if} +
+ + +{#if showModal} + +{/if} + +