🔧 chore: misc updates across contacts, mail, and shared-ui

- Contacts app improvements and fixes
- Mail IMAP sync provider updates
- Shared UI package updates
- Updated pnpm-lock.yaml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-03 16:13:50 +01:00
parent 043acf33bd
commit cda300440d
11 changed files with 4773 additions and 2185 deletions

View file

@ -0,0 +1,539 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { contacts } from './schema';
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/contacts';
// User ID - can be set via environment variable or defaults to test user
const USER_ID = process.env.SEED_USER_ID || 'seed-user-001';
interface SeedContact {
firstName: string;
lastName: string;
email?: string;
phone?: string;
mobile?: string;
company?: string;
jobTitle?: string;
street?: string;
city?: string;
postalCode?: string;
country?: string;
notes?: string;
isFavorite?: boolean;
}
const seedContacts: SeedContact[] = [
// Tech Industry
{
firstName: 'Anna',
lastName: 'Müller',
email: 'anna.mueller@techstartup.de',
phone: '+49 30 12345678',
mobile: '+49 170 1234567',
company: 'TechStartup GmbH',
jobTitle: 'CEO',
street: 'Alexanderplatz 1',
city: 'Berlin',
postalCode: '10178',
country: 'Deutschland',
notes: 'Kennengelernt auf der TechConf 2024. Interessiert an AI-Lösungen.',
isFavorite: true,
},
{
firstName: 'Max',
lastName: 'Schmidt',
email: 'max.schmidt@devhouse.io',
phone: '+49 89 98765432',
mobile: '+49 171 9876543',
company: 'DevHouse Solutions',
jobTitle: 'Lead Developer',
street: 'Marienplatz 8',
city: 'München',
postalCode: '80331',
country: 'Deutschland',
notes: 'Full-Stack Entwickler, spezialisiert auf React und Node.js',
isFavorite: true,
},
{
firstName: 'Sophie',
lastName: 'Weber',
email: 'sophie.weber@cloudify.com',
mobile: '+49 172 5551234',
company: 'Cloudify AG',
jobTitle: 'Cloud Architect',
city: 'Hamburg',
country: 'Deutschland',
},
{
firstName: 'Felix',
lastName: 'Becker',
email: 'felix.becker@datainsights.de',
phone: '+49 69 44556677',
company: 'Data Insights',
jobTitle: 'Data Scientist',
street: 'Zeil 15',
city: 'Frankfurt',
postalCode: '60313',
country: 'Deutschland',
notes: 'Machine Learning Experte',
},
{
firstName: 'Laura',
lastName: 'Klein',
email: 'laura.klein@uxdesign.studio',
mobile: '+49 173 8889990',
company: 'UX Design Studio',
jobTitle: 'UX Designer',
city: 'Köln',
country: 'Deutschland',
isFavorite: true,
},
// Business & Finance
{
firstName: 'Thomas',
lastName: 'Hoffmann',
email: 'thomas.hoffmann@financeplus.de',
phone: '+49 211 7778899',
mobile: '+49 174 1112233',
company: 'FinancePlus Consulting',
jobTitle: 'Managing Director',
street: 'Königsallee 27',
city: 'Düsseldorf',
postalCode: '40212',
country: 'Deutschland',
notes: 'Spezialist für Unternehmensfinanzierung',
},
{
firstName: 'Julia',
lastName: 'Fischer',
email: 'julia.fischer@legalexperts.de',
phone: '+49 30 5556789',
company: 'Legal Experts',
jobTitle: 'Rechtsanwältin',
city: 'Berlin',
country: 'Deutschland',
notes: 'Spezialisiert auf IT-Recht und Datenschutz',
},
{
firstName: 'Michael',
lastName: 'Wagner',
email: 'm.wagner@investcorp.de',
phone: '+49 89 3334455',
mobile: '+49 175 6667788',
company: 'InvestCorp',
jobTitle: 'Investment Manager',
city: 'München',
country: 'Deutschland',
isFavorite: true,
},
{
firstName: 'Christina',
lastName: 'Braun',
email: 'christina.braun@taxadvisors.de',
phone: '+49 711 9998877',
company: 'Tax Advisors Stuttgart',
jobTitle: 'Steuerberaterin',
street: 'Königstraße 42',
city: 'Stuttgart',
postalCode: '70173',
country: 'Deutschland',
},
// Creative & Media
{
firstName: 'David',
lastName: 'Zimmermann',
email: 'david@creativemind.agency',
mobile: '+49 176 2223344',
company: 'CreativeMind Agency',
jobTitle: 'Creative Director',
city: 'Berlin',
country: 'Deutschland',
notes: 'Award-winning Kreativagentur für Branding',
},
{
firstName: 'Nina',
lastName: 'Krause',
email: 'nina.krause@mediahouse.de',
phone: '+49 40 1234000',
company: 'MediaHouse Hamburg',
jobTitle: 'Content Manager',
city: 'Hamburg',
country: 'Deutschland',
},
{
firstName: 'Patrick',
lastName: 'Lehmann',
email: 'patrick@photostudio.com',
mobile: '+49 177 4445566',
company: 'Lehmann Fotografie',
jobTitle: 'Fotograf',
city: 'Köln',
country: 'Deutschland',
notes: 'Spezialisiert auf Produktfotografie und Events',
isFavorite: true,
},
// Healthcare
{
firstName: 'Dr. Sarah',
lastName: 'König',
email: 'dr.koenig@praxis-koenig.de',
phone: '+49 30 8889900',
company: 'Praxis Dr. König',
jobTitle: 'Allgemeinmedizinerin',
street: 'Friedrichstraße 120',
city: 'Berlin',
postalCode: '10117',
country: 'Deutschland',
},
{
firstName: 'Dr. Martin',
lastName: 'Schäfer',
email: 'martin.schaefer@klinikum.de',
phone: '+49 89 4445500',
company: 'Klinikum München',
jobTitle: 'Oberarzt Kardiologie',
city: 'München',
country: 'Deutschland',
},
// Education
{
firstName: 'Prof. Elisabeth',
lastName: 'Hartmann',
email: 'hartmann@uni-berlin.de',
phone: '+49 30 9876000',
company: 'Freie Universität Berlin',
jobTitle: 'Professorin für Informatik',
city: 'Berlin',
country: 'Deutschland',
notes: 'Forschungsschwerpunkt: Künstliche Intelligenz',
},
{
firstName: 'Andreas',
lastName: 'Richter',
email: 'a.richter@gymnasium.de',
phone: '+49 711 5554433',
company: 'Schiller-Gymnasium',
jobTitle: 'Schulleiter',
city: 'Stuttgart',
country: 'Deutschland',
},
// Retail & Gastronomy
{
firstName: 'Stefanie',
lastName: 'Wolf',
email: 'stefanie@wolfs-bistro.de',
phone: '+49 69 1112200',
mobile: '+49 178 3334455',
company: "Wolf's Bistro",
jobTitle: 'Inhaberin',
street: 'Goethestraße 15',
city: 'Frankfurt',
postalCode: '60313',
country: 'Deutschland',
notes: 'Tolle Location für Team-Events',
},
{
firstName: 'Oliver',
lastName: 'Neumann',
email: 'oliver@weinhandel-neumann.de',
phone: '+49 6131 778899',
company: 'Weinhandel Neumann',
jobTitle: 'Sommelier',
city: 'Mainz',
country: 'Deutschland',
},
// International Contacts
{
firstName: 'James',
lastName: 'Wilson',
email: 'james.wilson@techcorp.com',
phone: '+1 415 555 0123',
company: 'TechCorp Inc.',
jobTitle: 'VP Engineering',
city: 'San Francisco',
country: 'USA',
notes: 'Met at Web Summit. Interested in European expansion.',
isFavorite: true,
},
{
firstName: 'Marie',
lastName: 'Dubois',
email: 'marie.dubois@startup.fr',
phone: '+33 1 42 68 53 00',
mobile: '+33 6 12 34 56 78',
company: 'Paris Startup Hub',
jobTitle: 'Directrice',
street: '25 Rue de Rivoli',
city: 'Paris',
postalCode: '75001',
country: 'Frankreich',
},
{
firstName: 'Marco',
lastName: 'Rossi',
email: 'marco.rossi@designitalia.it',
mobile: '+39 333 123 4567',
company: 'Design Italia',
jobTitle: 'Art Director',
city: 'Mailand',
country: 'Italien',
},
{
firstName: 'Yuki',
lastName: 'Tanaka',
email: 'y.tanaka@techcompany.jp',
phone: '+81 3 1234 5678',
company: 'Tech Company Tokyo',
jobTitle: 'Product Manager',
city: 'Tokyo',
country: 'Japan',
},
// Freelancers & Consultants
{
firstName: 'Sebastian',
lastName: 'Maier',
email: 'seb.maier@freelance.de',
mobile: '+49 179 1234567',
jobTitle: 'Freelance Developer',
city: 'Berlin',
country: 'Deutschland',
notes: 'Vue.js und Python Spezialist. Verfügbar ab Q2.',
},
{
firstName: 'Katharina',
lastName: 'Peters',
email: 'k.peters@marketing-beratung.de',
mobile: '+49 170 9876543',
company: 'Peters Marketing Beratung',
jobTitle: 'Marketing Consultant',
city: 'Hamburg',
country: 'Deutschland',
isFavorite: true,
},
{
firstName: 'Daniel',
lastName: 'Schneider',
email: 'daniel@agile-coach.de',
mobile: '+49 171 5556677',
jobTitle: 'Agile Coach',
city: 'München',
country: 'Deutschland',
notes: 'Certified Scrum Master, 10+ Jahre Erfahrung',
},
// Personal Contacts
{
firstName: 'Lisa',
lastName: 'Berger',
email: 'lisa.berger@gmail.com',
mobile: '+49 172 1112233',
city: 'Köln',
country: 'Deutschland',
notes: 'Freundin aus Studienzeit',
isFavorite: true,
},
{
firstName: 'Markus',
lastName: 'Schulze',
email: 'markus.schulze@web.de',
mobile: '+49 173 4445566',
city: 'Düsseldorf',
country: 'Deutschland',
notes: 'Tennispartner',
},
{
firstName: 'Eva',
lastName: 'Friedrich',
email: 'eva.friedrich@outlook.com',
phone: '+49 30 1234321',
street: 'Prenzlauer Allee 42',
city: 'Berlin',
postalCode: '10405',
country: 'Deutschland',
notes: 'Nachbarin',
},
// More Tech
{
firstName: 'Tobias',
lastName: 'Keller',
email: 'tobias@backend-solutions.de',
mobile: '+49 174 7778899',
company: 'Backend Solutions',
jobTitle: 'Backend Engineer',
city: 'Frankfurt',
country: 'Deutschland',
},
{
firstName: 'Hannah',
lastName: 'Vogel',
email: 'hannah.vogel@securityfirst.de',
phone: '+49 30 9998877',
company: 'Security First GmbH',
jobTitle: 'Security Analyst',
city: 'Berlin',
country: 'Deutschland',
notes: 'Expertin für Penetration Testing',
},
{
firstName: 'Jan',
lastName: 'Schwarz',
email: 'jan@mobile-apps.de',
mobile: '+49 175 2223344',
company: 'Mobile Apps Studio',
jobTitle: 'iOS Developer',
city: 'Hamburg',
country: 'Deutschland',
},
{
firstName: 'Melanie',
lastName: 'Krüger',
email: 'melanie.krueger@ailab.de',
phone: '+49 89 6665544',
company: 'AI Lab München',
jobTitle: 'ML Engineer',
city: 'München',
country: 'Deutschland',
isFavorite: true,
},
{
firstName: 'Robert',
lastName: 'Lang',
email: 'robert.lang@devops-pro.de',
mobile: '+49 176 8889900',
company: 'DevOps Pro',
jobTitle: 'DevOps Engineer',
city: 'Stuttgart',
country: 'Deutschland',
notes: 'AWS und Kubernetes Spezialist',
},
// More Business
{
firstName: 'Claudia',
lastName: 'Bauer',
email: 'claudia@hr-consulting.de',
phone: '+49 211 3332211',
company: 'HR Consulting Plus',
jobTitle: 'HR Director',
city: 'Düsseldorf',
country: 'Deutschland',
},
{
firstName: 'Frank',
lastName: 'Huber',
email: 'f.huber@salesforce-partner.de',
mobile: '+49 177 1234567',
company: 'CRM Solutions',
jobTitle: 'Sales Director',
city: 'Frankfurt',
country: 'Deutschland',
},
{
firstName: 'Sabine',
lastName: 'Walter',
email: 'sabine@walter-events.de',
phone: '+49 30 4443322',
mobile: '+49 178 5556677',
company: 'Walter Events',
jobTitle: 'Event Manager',
city: 'Berlin',
country: 'Deutschland',
notes: 'Organisiert unsere Firmenevents',
isFavorite: true,
},
// Minimal contacts (just name and one contact method)
{
firstName: 'Peter',
lastName: 'Engel',
email: 'peter.engel@email.de',
},
{
firstName: 'Maria',
lastName: 'Sommer',
mobile: '+49 170 1111222',
},
{
firstName: 'Alexander',
lastName: 'Winter',
phone: '+49 40 9998877',
company: 'Winter & Partner',
},
];
async function seed() {
console.log('🌱 Starting seed...');
console.log(`📊 Preparing to insert ${seedContacts.length} contacts`);
const connection = postgres(DATABASE_URL);
const db = drizzle(connection);
try {
// Clear existing contacts for the test user
console.log('🧹 Clearing existing seed contacts...');
const { sql } = await import('drizzle-orm');
await db.delete(contacts).where(sql`${contacts.userId} = ${USER_ID}`);
// Insert seed contacts
console.log('📝 Inserting contacts...');
const contactsToInsert = seedContacts.map((contact) => ({
userId: USER_ID,
createdBy: USER_ID,
firstName: contact.firstName,
lastName: contact.lastName,
displayName: `${contact.firstName} ${contact.lastName}`,
email: contact.email || null,
phone: contact.phone || null,
mobile: contact.mobile || null,
company: contact.company || null,
jobTitle: contact.jobTitle || null,
street: contact.street || null,
city: contact.city || null,
postalCode: contact.postalCode || null,
country: contact.country || null,
notes: contact.notes || null,
isFavorite: contact.isFavorite || false,
isArchived: false,
visibility: 'private',
}));
await db.insert(contacts).values(contactsToInsert);
console.log(`✅ Successfully inserted ${seedContacts.length} contacts`);
console.log('');
console.log('📋 Summary:');
console.log(` - Favorites: ${seedContacts.filter((c) => c.isFavorite).length}`);
console.log(` - With company: ${seedContacts.filter((c) => c.company).length}`);
console.log(` - International: ${seedContacts.filter((c) => c.country && c.country !== 'Deutschland').length}`);
console.log('');
console.log(`🔑 User ID: ${USER_ID}`);
console.log('');
console.log('💡 To see these contacts, log in with a user that has this ID.');
console.log(' Or run with: SEED_USER_ID=your-user-id pnpm db:seed');
} catch (error) {
console.error('❌ Seed failed:', error);
throw error;
} finally {
await connection.end();
}
}
seed()
.then(() => {
console.log('🎉 Seed completed!');
process.exit(0);
})
.catch((error) => {
console.error('💥 Seed error:', error);
process.exit(1);
});

View file

@ -120,40 +120,45 @@ export const contactsApi = {
return fetchWithAuth(`/contacts${query ? `?${query}` : ''}`);
},
async get(id: string) {
return fetchWithAuth(`/contacts/${id}`);
async get(id: string): Promise<Contact> {
const response = await fetchWithAuth(`/contacts/${id}`);
return response.contact;
},
async create(data: Partial<Contact>) {
return fetchWithAuth('/contacts', {
async create(data: Partial<Contact>): Promise<Contact> {
const response = await fetchWithAuth('/contacts', {
method: 'POST',
body: JSON.stringify(data),
});
return response.contact;
},
async update(id: string, data: Partial<Contact>) {
return fetchWithAuth(`/contacts/${id}`, {
async update(id: string, data: Partial<Contact>): Promise<Contact> {
const response = await fetchWithAuth(`/contacts/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
return response.contact;
},
async delete(id: string) {
return fetchWithAuth(`/contacts/${id}`, {
async delete(id: string): Promise<void> {
await fetchWithAuth(`/contacts/${id}`, {
method: 'DELETE',
});
},
async toggleFavorite(id: string) {
return fetchWithAuth(`/contacts/${id}/favorite`, {
async toggleFavorite(id: string): Promise<Contact> {
const response = await fetchWithAuth(`/contacts/${id}/favorite`, {
method: 'POST',
});
return response.contact;
},
async toggleArchive(id: string) {
return fetchWithAuth(`/contacts/${id}/archive`, {
async toggleArchive(id: string): Promise<Contact> {
const response = await fetchWithAuth(`/contacts/${id}/archive`, {
method: 'POST',
});
return response.contact;
},
};

File diff suppressed because it is too large Load diff

View file

@ -64,9 +64,9 @@ export const contactsStore = {
error = null;
try {
const result = await contactsApi.get(id);
selectedContact = result.contact;
return result.contact;
const contact = await contactsApi.get(id);
selectedContact = contact;
return contact;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load contact';
console.error('Failed to load contact:', e);
@ -84,11 +84,11 @@ export const contactsStore = {
error = null;
try {
const result = await contactsApi.create(data);
const contact = await contactsApi.create(data);
// Add to local state
contacts = [result.contact, ...contacts];
contacts = [contact, ...contacts];
total += 1;
return result.contact;
return contact;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create contact';
console.error('Failed to create contact:', e);
@ -106,13 +106,13 @@ export const contactsStore = {
error = null;
try {
const result = await contactsApi.update(id, data);
const contact = await contactsApi.update(id, data);
// Update in local state
contacts = contacts.map((c) => (c.id === id ? result.contact : c));
contacts = contacts.map((c) => (c.id === id ? contact : c));
if (selectedContact?.id === id) {
selectedContact = result.contact;
selectedContact = contact;
}
return result.contact;
return contact;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to update contact';
console.error('Failed to update contact:', e);
@ -151,13 +151,13 @@ export const contactsStore = {
*/
async toggleFavorite(id: string) {
try {
const result = await contactsApi.toggleFavorite(id);
const contact = await contactsApi.toggleFavorite(id);
// Update in local state
contacts = contacts.map((c) => (c.id === id ? result.contact : c));
contacts = contacts.map((c) => (c.id === id ? contact : c));
if (selectedContact?.id === id) {
selectedContact = result.contact;
selectedContact = contact;
}
return result.contact;
return contact;
} catch (e) {
console.error('Failed to toggle favorite:', e);
throw e;
@ -169,14 +169,14 @@ export const contactsStore = {
*/
async toggleArchive(id: string) {
try {
const result = await contactsApi.toggleArchive(id);
const contact = await contactsApi.toggleArchive(id);
// Remove from current view if archived/unarchived
contacts = contacts.filter((c) => c.id !== id);
total -= 1;
if (selectedContact?.id === id) {
selectedContact = null;
}
return result.contact;
return contact;
} catch (e) {
console.error('Failed to toggle archive:', e);
throw e;

View file

@ -16,6 +16,13 @@
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
import { setLocale, supportedLocales } from '$lib/i18n';
import ContactDetailModal from '$lib/components/ContactDetailModal.svelte';
import { contactsStore } from '$lib/stores/contacts.svelte';
// Check if we're on a contact detail route
const contactDetailMatch = $derived($page.url.pathname.match(/^\/contacts\/([0-9a-f-]{36})$/i));
const showContactModal = $derived(!!contactDetailMatch);
const modalContactId = $derived(contactDetailMatch?.[1] || null);
// App switcher items
const appItems = getPillAppItems('contacts');
@ -131,6 +138,12 @@
goto('/login');
}
async function handleCloseContactModal() {
// Refresh contacts list in case something was changed
await contactsStore.loadContacts();
goto('/', { replaceState: false });
}
onMount(async () => {
// Redirect to login if not authenticated
if (!authStore.isAuthenticated) {
@ -206,6 +219,11 @@
{@render children()}
</div>
</main>
<!-- Contact Detail Modal -->
{#if showContactModal && modalContactId}
<ContactDetailModal contactId={modalContactId} onClose={handleCloseContactModal} />
{/if}
</div>
<style>

View file

@ -1,179 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { contactsStore } from '$lib/stores/contacts.svelte';
import { goto } from '$app/navigation';
import ContactList from '$lib/components/ContactList.svelte';
import '$lib/i18n';
let searchQuery = $state('');
let searchTimeout: ReturnType<typeof setTimeout>;
function handleSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
contactsStore.setSearch(searchQuery);
contactsStore.loadContacts();
}, 300);
}
function getInitials(contact: (typeof contactsStore.contacts)[0]) {
const first = contact.firstName?.[0] || '';
const last = contact.lastName?.[0] || '';
return (first + last).toUpperCase() || contact.email?.[0]?.toUpperCase() || '?';
}
function getDisplayName(contact: (typeof contactsStore.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';
}
async function handleToggleFavorite(e: MouseEvent, id: string) {
e.stopPropagation();
await contactsStore.toggleFavorite(id);
}
function handleContactClick(id: string) {
goto(`/contacts/${id}`);
}
onMount(async () => {
await contactsStore.loadContacts();
});
</script>
<svelte:head>
<title>{$_('contacts.title')} - Contacts</title>
</svelte:head>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-foreground">{$_('contacts.title')}</h1>
<a href="/contacts/new" class="btn btn-primary flex items-center gap-2">
<span>+</span>
<span>{$_('contacts.new')}</span>
</a>
</div>
<!-- Search -->
<div class="relative">
<input
type="text"
placeholder={$_('contacts.search')}
bind:value={searchQuery}
oninput={handleSearch}
class="input w-full pl-10"
/>
<svg
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<!-- Loading state -->
{#if contactsStore.loading}
<div class="flex justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
></div>
</div>
{:else if contactsStore.contacts.length === 0}
<!-- Empty state -->
<div class="text-center py-12">
<div class="text-6xl mb-4">👤</div>
<h2 class="text-xl font-semibold text-foreground mb-2">{$_('contacts.noContacts')}</h2>
<p class="text-muted-foreground mb-4">{$_('contacts.addFirst')}</p>
<a href="/contacts/new" class="btn btn-primary">
{$_('contacts.new')}
</a>
</div>
{:else}
<!-- Contacts List -->
<div class="space-y-2">
{#each contactsStore.contacts as contact (contact.id)}
<div
role="button"
tabindex="0"
onclick={() => handleContactClick(contact.id)}
onkeydown={(e) => e.key === 'Enter' && handleContactClick(contact.id)}
class="contact-card w-full text-left cursor-pointer"
>
<!-- Avatar -->
<div class="avatar">
{#if contact.photoUrl}
<img
src={contact.photoUrl}
alt={getDisplayName(contact)}
class="h-full w-full rounded-full object-cover"
/>
{:else}
{getInitials(contact)}
{/if}
</div>
<!-- Contact Info -->
<div class="flex-1 min-w-0">
<div class="font-medium text-foreground truncate">
{getDisplayName(contact)}
</div>
{#if contact.company || contact.jobTitle}
<div class="text-sm text-muted-foreground truncate">
{[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')}
</div>
{/if}
{#if contact.email}
<div class="text-sm text-muted-foreground truncate">
{contact.email}
</div>
{/if}
</div>
<!-- Favorite button -->
<button
onclick={(e) => handleToggleFavorite(e, contact.id)}
class="p-2 rounded-full hover:bg-accent transition-colors"
>
{#if contact.isFavorite}
<svg class="h-5 w-5 text-red-500 fill-current" viewBox="0 0 24 24">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
/>
</svg>
{:else}
<svg
class="h-5 w-5 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
{/if}
</button>
</div>
{/each}
</div>
<!-- Total count -->
<p class="text-sm text-muted-foreground text-center">
{contactsStore.total} Kontakte
</p>
{/if}
</div>
<ContactList />

View file

@ -113,6 +113,10 @@ export class ImapProvider implements EmailProvider {
const uid = parseInt(externalId, 10);
for await (const message of this.client.fetch(uid, { source: true }, { uid: true })) {
if (!message.source) {
this.logger.warn(`Email ${externalId} has no source`);
continue;
}
const parsed = await simpleParser(message.source);
return this.parseEmail(message, parsed);
}
@ -161,6 +165,10 @@ export class ImapProvider implements EmailProvider {
{ uid: true }
)) {
try {
if (!message.source) {
this.logger.warn(`Email UID ${message.uid} has no source`);
continue;
}
const parsed = await simpleParser(message.source);
const email = this.parseEmail(message, parsed);
emails.push(email);