From 437d612e81b371a6d2a96bafa57c7180a3e32732 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:12:19 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(contacts):=20remo?= =?UTF-8?q?ve=20statistics,=20network=20view=20and=20session=20storage;=20?= =?UTF-8?q?implement=20demo=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove statistics feature (stores, routes, ~560 LOC) - Remove network view with D3.js graph (~1,100 LOC) - Remove session-based contact storage - Add demo contacts for unauthenticated users (10 sample contacts) - Add auth gate prompts for create/edit/delete/favorite actions - Update layout with Demo-Modus banner and event handling - ~1,760 lines of code removed for simpler, cleaner codebase Co-Authored-By: Claude Opus 4.5 --- apps/contacts/apps/web/src/lib/api/network.ts | 74 --- .../src/lib/components/AuthGateModal.svelte | 22 +- .../lib/components/ContactDetailModal.svelte | 20 + .../web/src/lib/components/ContactList.svelte | 38 +- .../components/ContactsToolbarContent.svelte | 326 ++--------- .../src/lib/components/NewContactModal.svelte | 7 + .../components/network/NetworkControls.svelte | 374 ------------ .../components/network/NetworkGraph.svelte | 492 ---------------- .../views/ContactNetworkView.svelte | 257 --------- .../apps/web/src/lib/data/demo-contacts.ts | 215 +++++++ .../web/src/lib/stores/contacts.svelte.ts | 71 +++ .../apps/web/src/lib/stores/network.svelte.ts | 539 ------------------ .../src/lib/stores/session-contacts.svelte.ts | 235 -------- .../web/src/lib/stores/statistics.svelte.ts | 275 --------- .../apps/web/src/routes/(app)/+layout.svelte | 59 +- .../src/routes/(app)/statistics/+page.svelte | 281 --------- apps/contacts/docs/CLEANUP_PLAN.md | 100 ++++ 17 files changed, 517 insertions(+), 2868 deletions(-) delete mode 100644 apps/contacts/apps/web/src/lib/api/network.ts delete mode 100644 apps/contacts/apps/web/src/lib/components/network/NetworkControls.svelte delete mode 100644 apps/contacts/apps/web/src/lib/components/network/NetworkGraph.svelte delete mode 100644 apps/contacts/apps/web/src/lib/components/views/ContactNetworkView.svelte create mode 100644 apps/contacts/apps/web/src/lib/data/demo-contacts.ts delete mode 100644 apps/contacts/apps/web/src/lib/stores/network.svelte.ts delete mode 100644 apps/contacts/apps/web/src/lib/stores/session-contacts.svelte.ts delete mode 100644 apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts delete mode 100644 apps/contacts/apps/web/src/routes/(app)/statistics/+page.svelte create mode 100644 apps/contacts/docs/CLEANUP_PLAN.md diff --git a/apps/contacts/apps/web/src/lib/api/network.ts b/apps/contacts/apps/web/src/lib/api/network.ts deleted file mode 100644 index a7145170a..000000000 --- a/apps/contacts/apps/web/src/lib/api/network.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { authStore } from '$lib/stores/auth.svelte'; -import { API_BASE } from './config'; - -async function fetchWithAuth(url: string, options: RequestInit = {}) { - let token: string | null = null; - try { - token = await authStore.getAccessToken(); - console.log('[Network API] Got token:', token ? 'present' : 'missing'); - } catch (e) { - console.error('[Network API] Error getting token:', e); - } - - const headers: HeadersInit = { - 'Content-Type': 'application/json', - ...(options.headers || {}), - }; - - if (token) { - (headers as Record)['Authorization'] = `Bearer ${token}`; - } - - const fullUrl = `${API_BASE}${url}`; - console.log('[Network API] Fetching:', fullUrl); - - const response = await fetch(fullUrl, { - ...options, - headers, - }); - - console.log('[Network API] Response status:', response.status); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[Network API] Error response:', errorText); - let error: { message?: string } = { message: 'Request failed' }; - try { - error = JSON.parse(errorText); - } catch { - error = { message: errorText || 'Request failed' }; - } - throw new Error(error.message || 'Request failed'); - } - - return response.json(); -} - -export interface NetworkNode { - id: string; - name: string; - photoUrl: string | null; - company: string | null; - isFavorite: boolean; - tags: { id: string; name: string; color: string | null }[]; - connectionCount: number; -} - -export interface NetworkLink { - source: string; - target: string; - type: 'tag'; - strength: number; - sharedTags: string[]; -} - -export interface NetworkGraphResponse { - nodes: NetworkNode[]; - links: NetworkLink[]; -} - -export const networkApi = { - async getGraph(): Promise { - return fetchWithAuth('/network/graph'); - }, -}; diff --git a/apps/contacts/apps/web/src/lib/components/AuthGateModal.svelte b/apps/contacts/apps/web/src/lib/components/AuthGateModal.svelte index 9ec55e3f7..989bf869c 100644 --- a/apps/contacts/apps/web/src/lib/components/AuthGateModal.svelte +++ b/apps/contacts/apps/web/src/lib/components/AuthGateModal.svelte @@ -1,6 +1,5 @@
- {#if isNetworkMode} - + +
+ + contactsFilterStore.setSelectedTagId(typeof v === 'string' ? v : null)} + placeholder={$_('filters.allTags')} + embedded={true} + direction="up" + /> - -
- - + + contactsFilterStore.setContactFilter((typeof v === 'string' ? v : 'all') as ContactFilter)} + placeholder={$_('filters.contact.all')} + embedded={true} + direction="up" + /> + + + + contactsFilterStore.setBirthdayFilter( + (typeof v === 'string' ? v : 'all') as BirthdayFilter + )} + placeholder={$_('filters.birthday.all')} + embedded={true} + direction="up" + /> + + + {#if companyOptions.length > 0} + contactsFilterStore.setSelectedCompany(typeof v === 'string' ? v : null)} + placeholder={$_('filters.allCompanies')} + embedded={true} + direction="up" /> -
- -
- - -
- - - - -
- - - {#if hasActiveNetworkFilters} -
- {/if} - -
- {networkStore.nodes.length} {$_('contacts.contactsPlural')} - - {networkStore.links.length} {$_('network.connections')} -
- {:else} - + + {#if activeFilterCount > 0} + + {/if} +
- -
- - contactsFilterStore.setSelectedTagId(typeof v === 'string' ? v : null)} - placeholder={$_('filters.allTags')} - embedded={true} - direction="up" - /> +
- - - contactsFilterStore.setContactFilter( - (typeof v === 'string' ? v : 'all') as ContactFilter - )} - placeholder={$_('filters.contact.all')} - embedded={true} - direction="up" - /> - - - - contactsFilterStore.setBirthdayFilter( - (typeof v === 'string' ? v : 'all') as BirthdayFilter - )} - placeholder={$_('filters.birthday.all')} - embedded={true} - direction="up" - /> - - - {#if companyOptions.length > 0} - contactsFilterStore.setSelectedCompany(typeof v === 'string' ? v : null)} - placeholder={$_('filters.allCompanies')} - embedded={true} - direction="up" - /> - {/if} - - - {#if activeFilterCount > 0} - - {/if} -
- -
- - - - {/if} + +
diff --git a/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte b/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte index 9dd640127..3372fffb4 100644 --- a/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte +++ b/apps/contacts/apps/web/src/lib/components/NewContactModal.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { contactsApi, photoApi } from '$lib/api/contacts'; import { contactsStore } from '$lib/stores/contacts.svelte'; + import { authStore } from '$lib/stores/auth.svelte'; import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte'; import SocialMediaFields from './forms/SocialMediaFields.svelte'; import DateFields from './forms/DateFields.svelte'; @@ -119,6 +120,12 @@ } async function handleSave() { + // Demo mode: show auth gate + if (!authStore.isAuthenticated) { + window.dispatchEvent(new CustomEvent('show-auth-gate')); + return; + } + if (!firstName && !lastName && !email) { error = 'Bitte mindestens Name oder E-Mail angeben'; return; diff --git a/apps/contacts/apps/web/src/lib/components/network/NetworkControls.svelte b/apps/contacts/apps/web/src/lib/components/network/NetworkControls.svelte deleted file mode 100644 index 5bf41fa17..000000000 --- a/apps/contacts/apps/web/src/lib/components/network/NetworkControls.svelte +++ /dev/null @@ -1,374 +0,0 @@ - - -
- -
- - - {#if searchInput} - - {/if} -
- - - - - -
- - - -
- - -
- - {networkStore.nodes.length} Kontakte - - - - {networkStore.links.length} Verbindungen - -
-
- - -{#if showFilters} -
-
- -
- - -
- - -
- - -
- - - {#if hasActiveFilters} - - {/if} -
-
-{/if} - - diff --git a/apps/contacts/apps/web/src/lib/components/network/NetworkGraph.svelte b/apps/contacts/apps/web/src/lib/components/network/NetworkGraph.svelte deleted file mode 100644 index 2fd293032..000000000 --- a/apps/contacts/apps/web/src/lib/components/network/NetworkGraph.svelte +++ /dev/null @@ -1,492 +0,0 @@ - - -
- - - - - {#each graphLinks as link} - {@const coords = getLinkCoords(link)} - {@const sourceId = typeof link.source === 'string' ? link.source : link.source.id} - {@const targetId = typeof link.target === 'string' ? link.target : link.target.id} - {@const isHighlighted = - networkStore.selectedNodeId && - (sourceId === networkStore.selectedNodeId || targetId === networkStore.selectedNodeId)} - - {link.sharedTags.join(', ')} - - {/each} - - - - - {#each graphNodes as node (node.id)} - {@const isSelected = node.id === networkStore.selectedNodeId} - {@const isConnected = isConnectedToSelected(node.id, graphLinks)} - {@const isDimmed = networkStore.selectedNodeId && !isConnected} - handleDragStart(e, node)} - onclick={() => handleNodeClick(node)} - ondblclick={() => handleNodeDoubleClick(node)} - role="button" - tabindex="0" - aria-label={node.name} - > - - - - - {#if node.photoUrl} - - - - - {:else} - - {getInitials(node.name)} - - {/if} - - - {#if node.isFavorite} - - - ⭐ - - {/if} - - - {#if node.connectionCount > 0} - - - {node.connectionCount} - - {/if} - - - - {node.name} - - - - {#if node.subtitle} - - {node.subtitle} - - {/if} - - {/each} - - - - - - {#if graphNodes.length === 0 && !networkStore.loading} -
-
🔗
-

Keine Verbindungen gefunden

-

- Kontakte werden verbunden, wenn sie gemeinsame Tags haben. Füge Tags zu deinen Kontakten - hinzu, um das Netzwerk zu sehen. -

-
- {/if} -
- - diff --git a/apps/contacts/apps/web/src/lib/components/views/ContactNetworkView.svelte b/apps/contacts/apps/web/src/lib/components/views/ContactNetworkView.svelte deleted file mode 100644 index 768a05779..000000000 --- a/apps/contacts/apps/web/src/lib/components/views/ContactNetworkView.svelte +++ /dev/null @@ -1,257 +0,0 @@ - - -
- - {#if networkStore.error} - - {/if} - - -
- {#if networkStore.loading} - - {:else} - - {/if} -
- - - {#if networkStore.selectedNodeId} - - {/if} -
- - diff --git a/apps/contacts/apps/web/src/lib/data/demo-contacts.ts b/apps/contacts/apps/web/src/lib/data/demo-contacts.ts new file mode 100644 index 000000000..21203d67e --- /dev/null +++ b/apps/contacts/apps/web/src/lib/data/demo-contacts.ts @@ -0,0 +1,215 @@ +/** + * Demo Contacts - Static sample contacts for unauthenticated users + * + * Shows a realistic contact list with various contact types to demonstrate + * the app's capabilities without requiring login. + */ + +import type { Contact } from '$lib/api/contacts'; + +/** + * Generate demo contacts + */ +export function generateDemoContacts(): Contact[] { + const now = new Date().toISOString(); + + const demoContacts: Contact[] = [ + { + id: 'demo_1', + userId: 'demo', + firstName: 'Anna', + lastName: 'Müller', + displayName: 'Anna Müller', + email: 'anna.mueller@example.com', + phone: '+49 30 12345678', + mobile: '+49 170 1234567', + company: 'Tech Solutions GmbH', + jobTitle: 'Product Manager', + city: 'Berlin', + country: 'Deutschland', + isFavorite: true, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_1', name: 'Arbeit', color: '#3b82f6' }], + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_2', + userId: 'demo', + firstName: 'Max', + lastName: 'Schmidt', + displayName: 'Max Schmidt', + email: 'max.schmidt@example.com', + mobile: '+49 171 9876543', + company: 'Design Studio', + jobTitle: 'UX Designer', + city: 'München', + country: 'Deutschland', + isFavorite: true, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_1', name: 'Arbeit', color: '#3b82f6' }], + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_3', + userId: 'demo', + firstName: 'Lisa', + lastName: 'Weber', + displayName: 'Lisa Weber', + email: 'lisa.w@example.com', + phone: '+49 40 87654321', + city: 'Hamburg', + country: 'Deutschland', + isFavorite: false, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_2', name: 'Freunde', color: '#22c55e' }], + birthday: '1992-03-15', + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_4', + userId: 'demo', + firstName: 'Thomas', + lastName: 'Becker', + displayName: 'Thomas Becker', + email: 'thomas.becker@example.com', + mobile: '+49 172 5555555', + company: 'Consulting Partners', + jobTitle: 'Senior Consultant', + city: 'Frankfurt', + country: 'Deutschland', + website: 'https://example.com', + isFavorite: false, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_1', name: 'Arbeit', color: '#3b82f6' }], + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_5', + userId: 'demo', + firstName: 'Sarah', + lastName: 'Klein', + displayName: 'Sarah Klein', + email: 'sarah.klein@example.com', + phone: '+49 221 1111111', + mobile: '+49 173 2222222', + city: 'Köln', + country: 'Deutschland', + isFavorite: false, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_3', name: 'Familie', color: '#f59e0b' }], + birthday: '1988-07-22', + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_6', + userId: 'demo', + firstName: 'Michael', + lastName: 'Hoffmann', + displayName: 'Michael Hoffmann', + email: 'm.hoffmann@example.com', + company: 'Startup Ventures', + jobTitle: 'CEO', + city: 'Berlin', + country: 'Deutschland', + linkedin: 'michael-hoffmann', + isFavorite: false, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_1', name: 'Arbeit', color: '#3b82f6' }], + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_7', + userId: 'demo', + firstName: 'Julia', + lastName: 'Fischer', + displayName: 'Julia Fischer', + mobile: '+49 174 3333333', + city: 'Stuttgart', + country: 'Deutschland', + isFavorite: false, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_2', name: 'Freunde', color: '#22c55e' }], + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_8', + userId: 'demo', + firstName: 'Dr. Stefan', + lastName: 'Wagner', + displayName: 'Dr. Stefan Wagner', + email: 'dr.wagner@praxis.de', + phone: '+49 89 4444444', + company: 'Praxis Dr. Wagner', + jobTitle: 'Arzt', + street: 'Hauptstraße 42', + city: 'München', + postalCode: '80331', + country: 'Deutschland', + isFavorite: false, + isArchived: false, + visibility: 'private', + notes: 'Hausarzt, Termine immer vormittags', + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_9', + userId: 'demo', + firstName: 'Emma', + lastName: 'Braun', + displayName: 'Emma Braun', + email: 'emma.b@example.com', + mobile: '+49 175 6666666', + company: 'Marketing Pro', + jobTitle: 'Marketing Director', + city: 'Düsseldorf', + country: 'Deutschland', + isFavorite: true, + isArchived: false, + visibility: 'private', + tags: [{ id: 'tag_1', name: 'Arbeit', color: '#3b82f6' }], + createdAt: now, + updatedAt: now, + }, + { + id: 'demo_10', + userId: 'demo', + firstName: 'Peter', + lastName: 'Schneider', + displayName: 'Peter Schneider', + email: 'peter.schneider@example.com', + phone: '+49 511 7777777', + city: 'Hannover', + country: 'Deutschland', + isFavorite: false, + isArchived: true, + visibility: 'private', + tags: [], + createdAt: now, + updatedAt: now, + }, + ]; + + return demoContacts; +} + +/** + * Check if a contact ID is a demo contact + */ +export function isDemoContact(id: string): boolean { + return id.startsWith('demo_'); +} 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 38d498433..94b8d5dc7 100644 --- a/apps/contacts/apps/web/src/lib/stores/contacts.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/contacts.svelte.ts @@ -1,9 +1,13 @@ /** * Contacts Store - Manages contacts state using Svelte 5 runes + * Authenticated users: contacts from API + * Demo mode: static sample contacts to showcase the app */ import { contactsApi } from '$lib/api/contacts'; import type { Contact, ContactFilters } from '$lib/api/contacts'; +import { authStore } from './auth.svelte'; +import { generateDemoContacts, isDemoContact } from '$lib/data/demo-contacts'; // Default page size for pagination const DEFAULT_PAGE_SIZE = 50; @@ -48,6 +52,7 @@ export const contactsStore = { /** * Load contacts with optional filters (resets to first page) + * In demo mode, loads static sample contacts */ async loadContacts(newFilters?: ContactFilters) { if (newFilters) { @@ -58,6 +63,35 @@ export const contactsStore = { error = null; currentOffset = 0; + // Demo mode: load static demo contacts + if (!authStore.isAuthenticated) { + let demoContacts = generateDemoContacts(); + + // Apply filters to demo contacts + if (filters.search) { + const search = filters.search.toLowerCase(); + demoContacts = demoContacts.filter( + (c) => + c.displayName?.toLowerCase().includes(search) || + c.email?.toLowerCase().includes(search) || + c.company?.toLowerCase().includes(search) + ); + } + if (filters.isFavorite !== undefined) { + demoContacts = demoContacts.filter((c) => c.isFavorite === filters.isFavorite); + } + if (filters.isArchived !== undefined) { + demoContacts = demoContacts.filter((c) => c.isArchived === filters.isArchived); + } + + contacts = demoContacts; + total = demoContacts.length; + hasMore = false; + loading = false; + return; + } + + // Authenticated: fetch from API try { const result = await contactsApi.list({ ...filters, @@ -127,8 +161,14 @@ export const contactsStore = { /** * Create a new contact + * Requires authentication - demo mode shows auth gate */ async createContact(data: Partial) { + // Demo mode: require authentication + if (!authStore.isAuthenticated) { + return { error: 'auth_required' as const }; + } + loading = true; error = null; @@ -149,8 +189,14 @@ export const contactsStore = { /** * Update a contact + * Demo contacts require authentication */ async updateContact(id: string, data: Partial) { + // Demo contact: require authentication + if (isDemoContact(id)) { + return { error: 'auth_required' as const }; + } + loading = true; error = null; @@ -173,8 +219,14 @@ export const contactsStore = { /** * Delete a contact + * Demo contacts require authentication */ async deleteContact(id: string) { + // Demo contact: require authentication + if (isDemoContact(id)) { + return { error: 'auth_required' as const }; + } + loading = true; error = null; @@ -197,8 +249,14 @@ export const contactsStore = { /** * Toggle favorite status + * Demo contacts require authentication */ async toggleFavorite(id: string) { + // Demo contact: require authentication + if (isDemoContact(id)) { + return { error: 'auth_required' as const }; + } + try { const contact = await contactsApi.toggleFavorite(id); // Update in local state @@ -215,8 +273,14 @@ export const contactsStore = { /** * Toggle archive status + * Demo contacts require authentication */ async toggleArchive(id: string) { + // Demo contact: require authentication + if (isDemoContact(id)) { + return { error: 'auth_required' as const }; + } + try { const contact = await contactsApi.toggleArchive(id); // Remove from current view if archived/unarchived @@ -260,4 +324,11 @@ export const contactsStore = { clearSelected() { selectedContact = null; }, + + /** + * Check if a contact is a demo contact (static sample data) + */ + isDemoContact(contactId: string) { + return isDemoContact(contactId); + }, }; diff --git a/apps/contacts/apps/web/src/lib/stores/network.svelte.ts b/apps/contacts/apps/web/src/lib/stores/network.svelte.ts deleted file mode 100644 index f0d2f8ea2..000000000 --- a/apps/contacts/apps/web/src/lib/stores/network.svelte.ts +++ /dev/null @@ -1,539 +0,0 @@ -/** - * Network Store - Manages network graph state with D3-force simulation - */ - -import { browser } from '$app/environment'; -import { networkApi } from '$lib/api/network'; -import type { NetworkNode, NetworkLink } from '$lib/api/network'; -import { - forceSimulation, - forceLink, - forceManyBody, - forceCenter, - forceCollide, - type Simulation, -} from 'd3-force'; -import type { - SimulationNode as SharedSimulationNode, - SimulationLink as SharedSimulationLink, -} from '@manacore/shared-ui'; - -// Re-export types from shared-ui for convenience -export type SimulationNode = SharedSimulationNode; -export type SimulationLink = SharedSimulationLink; - -// Interface for NetworkGraph component zoom methods -interface NetworkGraphZoomMethods { - zoomIn(): void; - zoomOut(): void; - resetZoom(): void; - focusOnSelectedNode(): void; -} - -// Graph component reference for zoom controls -let graphComponentRef: NetworkGraphZoomMethods | null = null; - -// localStorage key for toolbar state -const TOOLBAR_STORAGE_KEY = 'network-toolbar-state'; - -// Load toolbar state from localStorage -function loadToolbarState(): boolean { - if (!browser) return true; - try { - const stored = localStorage.getItem(TOOLBAR_STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - return parsed.isCollapsed ?? true; - } - } catch { - // Ignore parse errors - } - return true; -} - -// Save toolbar state to localStorage -function saveToolbarState(isCollapsed: boolean) { - if (!browser) return; - try { - localStorage.setItem(TOOLBAR_STORAGE_KEY, JSON.stringify({ isCollapsed })); - } catch { - // Ignore storage errors - } -} - -// State -let nodes = $state([]); -let links = $state([]); -let loading = $state(false); -let error = $state(null); -let selectedNodeId = $state(null); -let simulation: Simulation | null = null; -let searchQuery = $state(''); -let filterTagId = $state(null); -let filterCompany = $state(null); -let minStrength = $state(0); -let tickCounter = $state(0); // Used to trigger reactivity on simulation tick -let simulationInitialized = false; -let dataLoaded = false; // Prevent double loading -let lastDimensions = { width: 0, height: 0 }; -let isToolbarCollapsed = $state(loadToolbarState()); - -// Derived state for filtering -const filteredNodes = $derived.by(() => { - let result = nodes; - - // Search filter - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (node) => - node.name.toLowerCase().includes(query) || - node.subtitle?.toLowerCase().includes(query) || - node.tags.some((t) => t.name.toLowerCase().includes(query)) - ); - } - - // Tag filter - if (filterTagId) { - result = result.filter((node) => node.tags.some((t) => t.id === filterTagId)); - } - - // Company filter (uses subtitle field) - if (filterCompany) { - result = result.filter((node) => node.subtitle === filterCompany); - } - - return result; -}); - -const filteredLinks = $derived.by(() => { - const filteredNodeIds = new Set(filteredNodes.map((n) => n.id)); - return links.filter((link) => { - const sourceId = typeof link.source === 'string' ? link.source : link.source.id; - const targetId = typeof link.target === 'string' ? link.target : link.target.id; - // Check if both nodes are visible and strength meets minimum - if (!filteredNodeIds.has(sourceId) || !filteredNodeIds.has(targetId)) { - return false; - } - // Filter by minimum strength - if (minStrength > 0 && link.strength < minStrength) { - return false; - } - return true; - }); -}); - -// Get unique companies for filter dropdown (uses subtitle field) -const uniqueCompanies = $derived.by(() => { - const companies = new Set(); - for (const node of nodes) { - if (node.subtitle) { - companies.add(node.subtitle); - } - } - return Array.from(companies).sort(); -}); - -// Get unique tags for filter dropdown -const uniqueTags = $derived.by(() => { - const tagsMap = new Map(); - for (const node of nodes) { - for (const tag of node.tags) { - if (!tagsMap.has(tag.id)) { - tagsMap.set(tag.id, tag); - } - } - } - return Array.from(tagsMap.values()).sort((a, b) => a.name.localeCompare(b.name)); -}); - -export const networkStore = { - // Getters - get nodes() { - // Access tickCounter to trigger reactivity on simulation updates - void tickCounter; - return filteredNodes; - }, - get allNodes() { - void tickCounter; - return nodes; - }, - get links() { - void tickCounter; - return filteredLinks; - }, - get allLinks() { - void tickCounter; - return links; - }, - get tick() { - return tickCounter; - }, - get loading() { - return loading; - }, - get error() { - return error; - }, - get selectedNodeId() { - return selectedNodeId; - }, - get selectedNode() { - return nodes.find((n) => n.id === selectedNodeId) || null; - }, - get searchQuery() { - return searchQuery; - }, - get filterTagId() { - return filterTagId; - }, - get filterCompany() { - return filterCompany; - }, - get minStrength() { - return minStrength; - }, - get uniqueCompanies() { - return uniqueCompanies; - }, - get uniqueTags() { - return uniqueTags; - }, - get isToolbarCollapsed() { - return isToolbarCollapsed; - }, - - /** - * Set toolbar collapsed state - */ - setToolbarCollapsed(value: boolean) { - isToolbarCollapsed = value; - saveToolbarState(value); - }, - - /** - * Toggle toolbar collapsed state - */ - toggleToolbar() { - isToolbarCollapsed = !isToolbarCollapsed; - saveToolbarState(isToolbarCollapsed); - }, - - /** - * Register graph component reference for zoom controls - */ - setGraphComponent(component: NetworkGraphZoomMethods | null) { - graphComponentRef = component; - }, - - /** - * Zoom in on the graph - */ - zoomIn() { - graphComponentRef?.zoomIn(); - }, - - /** - * Zoom out on the graph - */ - zoomOut() { - graphComponentRef?.zoomOut(); - }, - - /** - * Reset zoom to fit all nodes - */ - resetZoom() { - graphComponentRef?.resetZoom(); - }, - - /** - * Focus on the currently selected node - */ - focusOnSelected() { - graphComponentRef?.focusOnSelectedNode(); - }, - - /** - * Load network graph data from API - */ - async loadGraph(force = false) { - // Prevent double loading - if (dataLoaded && !force) { - console.log('[Network] Data already loaded, skipping'); - return; - } - - if (loading) { - console.log('[Network] Already loading, skipping'); - return; - } - - loading = true; - error = null; - - // Reset simulation state for fresh data - if (simulation) { - simulation.stop(); - simulation = null; - } - simulationInitialized = false; - - try { - const response = await networkApi.getGraph(); - - console.log( - '[Network] Loaded', - response.nodes.length, - 'nodes and', - response.links.length, - 'links' - ); - - // Convert to simulation nodes with subtitle for company - nodes = response.nodes.map((node) => ({ - ...node, - subtitle: node.company, // Map company to subtitle for shared component - x: undefined, - y: undefined, - vx: undefined, - vy: undefined, - fx: null, - fy: null, - })); - - // Convert to simulation links - links = response.links.map((link) => ({ - source: link.source, - target: link.target, - type: link.type, - strength: link.strength, - sharedTags: link.sharedTags, - })); - - dataLoaded = true; - } catch (e) { - error = e instanceof Error ? e.message : 'Failed to load network graph'; - console.error('Failed to load network graph:', e); - } finally { - loading = false; - } - }, - - /** - * Initialize D3 force simulation - */ - initSimulation(width: number, height: number) { - if (!browser) return; - if (nodes.length === 0) return; - if (width <= 0 || height <= 0) return; - - // Prevent re-initialization if already running - if (simulationInitialized && simulation) { - // Only update center if dimensions changed significantly - if ( - Math.abs(lastDimensions.width - width) > 50 || - Math.abs(lastDimensions.height - height) > 50 - ) { - console.log('[Network] Updating simulation center for new dimensions:', width, 'x', height); - lastDimensions = { width, height }; - this.updateSimulationCenter(width, height); - } - return; - } - - // Stop existing simulation - if (simulation) { - simulation.stop(); - } - - console.log( - '[Network] Initializing simulation with', - nodes.length, - 'nodes, dimensions:', - width, - 'x', - height - ); - lastDimensions = { width, height }; - - // Initialize node positions spread around the center - const centerX = width / 2; - const centerY = height / 2; - const radius = Math.min(width, height) / 3; - - nodes.forEach((node, i) => { - // Only set initial position if not already set - if (node.x === undefined || node.y === undefined) { - // Spread nodes in a circle initially - const angle = (i / nodes.length) * 2 * Math.PI; - const r = radius * (0.5 + Math.random() * 0.5); - node.x = centerX + r * Math.cos(angle); - node.y = centerY + r * Math.sin(angle); - } - }); - - // Create new simulation - simulation = forceSimulation(nodes) - .force( - 'link', - forceLink(links) - .id((d) => d.id) - .distance(100) // Fixed distance for cleaner layout - .strength(0.5) - ) - .force('charge', forceManyBody().strength(-300)) - .force('center', forceCenter(centerX, centerY)) - .force('collision', forceCollide().radius(50)) - .on('tick', () => { - // Trigger Svelte reactivity by incrementing counter - tickCounter++; - }); - - simulationInitialized = true; - - // Run simulation with higher alpha for better initial spread - simulation.alpha(1).restart(); - }, - - /** - * Update simulation dimensions (e.g., on window resize) - */ - updateSimulationCenter(width: number, height: number) { - if (simulation) { - simulation.force('center', forceCenter(width / 2, height / 2)); - simulation.alpha(0.3).restart(); - } - }, - - /** - * Stop the simulation - */ - stopSimulation() { - if (simulation) { - simulation.stop(); - simulation = null; - } - simulationInitialized = false; - // Don't reset dataLoaded here - only reset when navigating away - }, - - /** - * Reset the store completely (call when leaving the page) - */ - reset() { - this.stopSimulation(); - nodes = []; - links = []; - dataLoaded = false; - lastDimensions = { width: 0, height: 0 }; - tickCounter = 0; - }, - - /** - * Reheat simulation (restart with some energy) - */ - reheatSimulation() { - if (simulation) { - simulation.alpha(0.3).restart(); - } - }, - - /** - * Fix node position (for dragging) - */ - fixNode(nodeId: string, x: number, y: number) { - const node = nodes.find((n) => n.id === nodeId); - if (node) { - node.fx = x; - node.fy = y; - } - }, - - /** - * Release node (after dragging) - */ - releaseNode(nodeId: string) { - const node = nodes.find((n) => n.id === nodeId); - if (node) { - node.fx = null; - node.fy = null; - } - }, - - /** - * Select a node - */ - selectNode(nodeId: string | null) { - selectedNodeId = nodeId; - }, - - /** - * Set search query - */ - setSearch(query: string) { - searchQuery = query; - }, - - /** - * Set tag filter - */ - setFilterTag(tagId: string | null) { - filterTagId = tagId; - }, - - /** - * Set company filter - */ - setFilterCompany(company: string | null) { - filterCompany = company; - }, - - /** - * Set minimum strength filter - */ - setMinStrength(strength: number) { - minStrength = strength; - }, - - /** - * Clear all filters - */ - clearFilters() { - searchQuery = ''; - filterTagId = null; - filterCompany = null; - minStrength = 0; - }, - - /** - * Get connected nodes for a given node - */ - getConnectedNodes(nodeId: string): SimulationNode[] { - const connectedIds = new Set(); - - for (const link of links) { - const sourceId = typeof link.source === 'string' ? link.source : link.source.id; - const targetId = typeof link.target === 'string' ? link.target : link.target.id; - - if (sourceId === nodeId) { - connectedIds.add(targetId); - } else if (targetId === nodeId) { - connectedIds.add(sourceId); - } - } - - return nodes.filter((n) => connectedIds.has(n.id)); - }, - - /** - * Get links for a given node - */ - getNodeLinks(nodeId: string): SimulationLink[] { - return links.filter((link) => { - const sourceId = typeof link.source === 'string' ? link.source : link.source.id; - const targetId = typeof link.target === 'string' ? link.target : link.target.id; - return sourceId === nodeId || targetId === nodeId; - }); - }, -}; diff --git a/apps/contacts/apps/web/src/lib/stores/session-contacts.svelte.ts b/apps/contacts/apps/web/src/lib/stores/session-contacts.svelte.ts deleted file mode 100644 index 0a4a91a26..000000000 --- a/apps/contacts/apps/web/src/lib/stores/session-contacts.svelte.ts +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Session Contacts Store - Temporary local contacts for guest users - * Contacts are stored in sessionStorage and lost when the browser tab is closed - */ - -import type { Contact } from '$lib/api/contacts'; -import { browser } from '$app/environment'; - -const STORAGE_KEY = 'contacts-session-contacts'; - -// Generate a unique ID for session contacts -function generateSessionId(): string { - return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; -} - -// Load contacts from sessionStorage -function loadFromStorage(): Contact[] { - if (!browser) return []; - try { - const stored = sessionStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : []; - } catch { - return []; - } -} - -// Save contacts to sessionStorage -function saveToStorage(contacts: Contact[]) { - if (!browser) return; - try { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(contacts)); - } catch (e) { - console.warn('Failed to save session contacts:', e); - } -} - -// State -let contacts = $state(loadFromStorage()); - -export const sessionContactsStore = { - get contacts() { - return contacts; - }, - - get hasContacts() { - return contacts.length > 0; - }, - - /** - * Initialize from sessionStorage (call on mount) - */ - initialize() { - contacts = loadFromStorage(); - }, - - /** - * Create a new session contact - */ - createContact(data: Partial): Contact { - const now = new Date().toISOString(); - const newContact: Contact = { - id: generateSessionId(), - userId: 'guest', - firstName: data.firstName || null, - lastName: data.lastName || null, - displayName: data.displayName || null, - nickname: data.nickname || null, - email: data.email || null, - phone: data.phone || null, - mobile: data.mobile || null, - street: data.street || null, - city: data.city || null, - postalCode: data.postalCode || null, - country: data.country || null, - company: data.company || null, - jobTitle: data.jobTitle || null, - department: data.department || null, - website: data.website || null, - birthday: data.birthday || null, - notes: data.notes || null, - photoUrl: data.photoUrl || null, - customDates: data.customDates || null, - linkedin: data.linkedin || null, - twitter: data.twitter || null, - facebook: data.facebook || null, - instagram: data.instagram || null, - xing: data.xing || null, - github: data.github || null, - youtube: data.youtube || null, - tiktok: data.tiktok || null, - telegram: data.telegram || null, - whatsapp: data.whatsapp || null, - signal: data.signal || null, - discord: data.discord || null, - bluesky: data.bluesky || null, - tags: [], - isFavorite: data.isFavorite || false, - isArchived: data.isArchived || false, - organizationId: null, - teamId: null, - visibility: 'private', - createdAt: now, - updatedAt: now, - }; - - contacts = [...contacts, newContact]; - saveToStorage(contacts); - return newContact; - }, - - /** - * Update a session contact - */ - updateContact(id: string, data: Partial): Contact | null { - const index = contacts.findIndex((c) => c.id === id); - if (index === -1) return null; - - const updatedContact = { - ...contacts[index], - ...data, - updatedAt: new Date().toISOString(), - }; - - contacts = contacts.map((c) => (c.id === id ? updatedContact : c)); - saveToStorage(contacts); - return updatedContact; - }, - - /** - * Toggle favorite status - */ - toggleFavorite(id: string): Contact | null { - const contact = contacts.find((c) => c.id === id); - if (!contact) return null; - return this.updateContact(id, { isFavorite: !contact.isFavorite }); - }, - - /** - * Toggle archive status - */ - toggleArchive(id: string): Contact | null { - const contact = contacts.find((c) => c.id === id); - if (!contact) return null; - return this.updateContact(id, { isArchived: !contact.isArchived }); - }, - - /** - * Delete a session contact - */ - deleteContact(id: string): boolean { - const hadContact = contacts.some((c) => c.id === id); - contacts = contacts.filter((c) => c.id !== id); - saveToStorage(contacts); - return hadContact; - }, - - /** - * Get contact by ID - */ - getById(id: string): Contact | undefined { - return contacts.find((c) => c.id === id); - }, - - /** - * Check if a contact ID is a session contact - */ - isSessionContact(id: string): boolean { - return id.startsWith('session_'); - }, - - /** - * Get all contacts (for migration to cloud on login) - */ - getAllContacts(): Contact[] { - return [...contacts]; - }, - - /** - * Clear all session contacts (after migration or on explicit clear) - */ - clear() { - contacts = []; - if (browser) { - sessionStorage.removeItem(STORAGE_KEY); - } - }, - - /** - * Get count of session contacts - */ - get count() { - return contacts.length; - }, - - /** - * Get favorite contacts - */ - get favoriteContacts() { - return contacts.filter((c) => c.isFavorite && !c.isArchived); - }, - - /** - * Get archived contacts - */ - get archivedContacts() { - return contacts.filter((c) => c.isArchived); - }, - - /** - * Get active (non-archived) contacts - */ - get activeContacts() { - return contacts.filter((c) => !c.isArchived); - }, - - /** - * Search session contacts - */ - search(query: string): Contact[] { - if (!query.trim()) return this.activeContacts; - const lower = query.toLowerCase(); - return this.activeContacts.filter((c) => { - const searchFields = [ - c.firstName, - c.lastName, - c.displayName, - c.email, - c.phone, - c.mobile, - c.company, - ]; - return searchFields.some((field) => field?.toLowerCase().includes(lower)); - }); - }, -}; diff --git a/apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts b/apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts deleted file mode 100644 index 4c2a112f6..000000000 --- a/apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts +++ /dev/null @@ -1,275 +0,0 @@ -/** - * Contacts Statistics Store - Calculates contact statistics using Svelte 5 runes - */ - -import type { Contact } from '$lib/api/contacts'; -import { subDays, format, parseISO, isWithinInterval, getMonth, eachDayOfInterval } from 'date-fns'; -import { de } from 'date-fns/locale'; -import type { - HeatmapDataPoint, - TrendDataPoint, - DonutSegment, - ProgressItem, -} from '@manacore/shared-ui'; - -// Types -export interface ContactTag { - id: string; - name: string; - color: string; -} - -// State -let contacts = $state([]); -let tags = $state([]); - -export const contactsStatisticsStore = { - // Setters - setContacts(newContacts: Contact[]) { - contacts = newContacts; - }, - - setTags(newTags: ContactTag[]) { - tags = newTags; - }, - - // Quick Stats - get totalContacts() { - return contacts.length; - }, - - get favoriteContacts() { - return contacts.filter((c) => c.isFavorite).length; - }, - - get archivedContacts() { - return contacts.filter((c) => c.isArchived).length; - }, - - get activeContacts() { - return contacts.filter((c) => !c.isArchived).length; - }, - - get recentlyAdded() { - const weekAgo = subDays(new Date(), 7); - return contacts.filter((c) => { - const createdAt = - typeof c.createdAt === 'string' ? parseISO(c.createdAt) : new Date(c.createdAt); - return createdAt >= weekAgo; - }).length; - }, - - get birthdaysThisMonth() { - const currentMonth = getMonth(new Date()); - return contacts.filter((c) => { - if (!c.birthday) return false; - const birthday = typeof c.birthday === 'string' ? parseISO(c.birthday) : new Date(c.birthday); - return getMonth(birthday) === currentMonth; - }).length; - }, - - get contactsWithEmail() { - return contacts.filter((c) => c.email).length; - }, - - get contactsWithPhone() { - return contacts.filter((c) => c.phone || c.mobile).length; - }, - - // Completeness rate (contacts with email AND phone) - get completenessRate() { - if (contacts.length === 0) return 0; - const complete = contacts.filter((c) => c.email && (c.phone || c.mobile)).length; - return Math.round((complete / contacts.length) * 100); - }, - - // Activity Heatmap (last 6 months) - based on contact creation - get activityHeatmap(): HeatmapDataPoint[] { - const endDate = new Date(); - const startDate = subDays(endDate, 180); - - // Count contacts created per day - const creationMap = new Map(); - - contacts.forEach((c) => { - const createdAt = - typeof c.createdAt === 'string' ? parseISO(c.createdAt) : new Date(c.createdAt); - if (createdAt >= startDate && createdAt <= endDate) { - const dateKey = format(createdAt, 'yyyy-MM-dd'); - creationMap.set(dateKey, (creationMap.get(dateKey) || 0) + 1); - } - }); - - // Generate all days - const days = eachDayOfInterval({ start: startDate, end: endDate }); - - return days.map((day) => { - const dateKey = format(day, 'yyyy-MM-dd'); - return { - date: dateKey, - count: creationMap.get(dateKey) || 0, - dayOfWeek: day.getDay(), - }; - }); - }, - - // Weekly Trend (last 4 weeks) - get weeklyTrend(): TrendDataPoint[] { - const endDate = new Date(); - const startDate = subDays(endDate, 27); - - const creationMap = new Map(); - - contacts.forEach((c) => { - const createdAt = - typeof c.createdAt === 'string' ? parseISO(c.createdAt) : new Date(c.createdAt); - if (createdAt >= startDate && createdAt <= endDate) { - const dateKey = format(createdAt, 'yyyy-MM-dd'); - creationMap.set(dateKey, (creationMap.get(dateKey) || 0) + 1); - } - }); - - const days = eachDayOfInterval({ start: startDate, end: endDate }); - - return days.map((day) => { - const dateKey = format(day, 'yyyy-MM-dd'); - return { - date: dateKey, - count: creationMap.get(dateKey) || 0, - label: format(day, 'EEE', { locale: de }), - }; - }); - }, - - // Contact Status Breakdown (Donut Chart) - Favorites / Active / Archived - get statusBreakdown(): DonutSegment[] { - const total = contacts.length; - if (total === 0) return []; - - const favorites = contacts.filter((c) => c.isFavorite && !c.isArchived).length; - const archived = contacts.filter((c) => c.isArchived).length; - const regular = contacts.filter((c) => !c.isFavorite && !c.isArchived).length; - - return [ - { - id: 'favorites', - label: 'Favoriten', - count: favorites, - percentage: Math.round((favorites / total) * 100), - color: '#F59E0B', // amber - }, - { - id: 'regular', - label: 'Aktiv', - count: regular, - percentage: Math.round((regular / total) * 100), - color: '#10B981', // green - }, - { - id: 'archived', - label: 'Archiviert', - count: archived, - percentage: Math.round((archived / total) * 100), - color: '#6B7280', // gray - }, - ]; - }, - - // Tags Progress (Progress Bars) - get tagProgress(): ProgressItem[] { - // Count contacts per tag - const tagCountMap = new Map(); - - // This requires contacts to have a tags array - we'll estimate from the tag data - // For now, we'll show tags with placeholder counts - // In a real implementation, we'd need contactTags relation data - - const result: ProgressItem[] = tags.map((tag) => ({ - id: tag.id, - name: tag.name, - color: tag.color || '#6B7280', - total: contacts.length, // Total contacts as reference - completed: 0, // Would need contact-tag relation to calculate - percentage: 0, - })); - - return result.sort((a, b) => b.completed - a.completed); - }, - - // Info completeness breakdown - get infoBreakdown(): DonutSegment[] { - const total = contacts.length; - if (total === 0) return []; - - const withEmail = contacts.filter((c) => c.email).length; - const withPhone = contacts.filter((c) => c.phone || c.mobile).length; - const withCompany = contacts.filter((c) => c.company).length; - const withBirthday = contacts.filter((c) => c.birthday).length; - - return [ - { - id: 'email', - label: 'Mit E-Mail', - count: withEmail, - percentage: Math.round((withEmail / total) * 100), - color: '#3B82F6', // blue - }, - { - id: 'phone', - label: 'Mit Telefon', - count: withPhone, - percentage: Math.round((withPhone / total) * 100), - color: '#10B981', // green - }, - { - id: 'company', - label: 'Mit Firma', - count: withCompany, - percentage: Math.round((withCompany / total) * 100), - color: '#8B5CF6', // violet - }, - { - id: 'birthday', - label: 'Mit Geburtstag', - count: withBirthday, - percentage: Math.round((withBirthday / total) * 100), - color: '#EC4899', // pink - }, - ]; - }, - - // Country breakdown - get countryBreakdown(): ProgressItem[] { - const countryMap = new Map(); - - contacts.forEach((c) => { - const country = c.country || 'Unbekannt'; - countryMap.set(country, (countryMap.get(country) || 0) + 1); - }); - - const result: ProgressItem[] = []; - const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#6B7280']; - let colorIndex = 0; - - countryMap.forEach((count, country) => { - if (country !== 'Unbekannt' || count > 0) { - result.push({ - id: country, - name: country, - color: colors[colorIndex % colors.length], - total: contacts.length, - completed: count, - percentage: Math.round((count / contacts.length) * 100), - }); - colorIndex++; - } - }); - - return result.sort((a, b) => b.completed - a.completed).slice(0, 8); - }, - - // Total tags count - get totalTags() { - return tags.length; - }, -}; diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index c694059be..9ac21ec6c 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -47,8 +47,8 @@ } from '$lib/utils/contact-parser'; import ContactsToolbar from '$lib/components/ContactsToolbar.svelte'; import AuthGateModal from '$lib/components/AuthGateModal.svelte'; - import { sessionContactsStore } from '$lib/stores/session-contacts.svelte'; import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui'; + import { browser } from '$app/environment'; // Tags state for Quick-Create let availableTags = $state<{ id: string; name: string }[]>([]); @@ -140,7 +140,6 @@ const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Kontakte', icon: 'users' }, { href: '/tags', label: 'Tags', icon: 'tag' }, - { href: '/statistics', label: 'Statistiken', icon: 'bar-chart-3' }, { href: '/settings', label: 'Einstellungen', icon: 'settings' }, { href: '/feedback', label: 'Feedback', icon: 'chat' }, { href: '/help', label: 'Hilfe', icon: 'help-circle' }, @@ -227,8 +226,17 @@ showAuthGateModal = true; } - // Session contacts indicator - let sessionContactCount = $derived(sessionContactsStore.count); + // Listen for show-auth-gate events from child components + $effect(() => { + if (browser) { + const handler = (e: Event) => { + const customEvent = e as CustomEvent<{ action?: 'save' | 'sync' | 'feature' }>; + showAuthGate(customEvent.detail?.action || 'save'); + }; + window.addEventListener('show-auth-gate', handler); + return () => window.removeEventListener('show-auth-gate', handler); + } + }); async function handleCloseContactModal() { // Refresh contacts list in case something was changed @@ -298,9 +306,6 @@ viewModeStore.initialize(); contactsFilterStore.initialize(); - // Initialize session contacts for guest mode - sessionContactsStore.initialize(); - // Show guest welcome modal for unauthenticated users if (!authStore.isAuthenticated && shouldShowGuestWelcome('contacts')) { showGuestWelcome = true; @@ -318,23 +323,6 @@ } catch (e) { console.error('Failed to load tags:', e); } - - // Check for session contacts to migrate after login - if (sessionContactsStore.hasContacts) { - // Migrate session contacts to cloud - const sessionContacts = sessionContactsStore.getAllContacts(); - for (const contact of sessionContacts) { - try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, userId, createdAt, updatedAt, ...contactData } = contact; - await contactsStore.createContact(contactData); - } catch (e) { - console.error('Failed to migrate session contact:', e); - } - } - // Clear session contacts after migration - sessionContactsStore.clear(); - } } // Initialize sidebar mode from localStorage @@ -366,7 +354,7 @@
- + {#if !authStore.isAuthenticated}
+ - Gast-Modus - {#if sessionContactCount > 0} - - {sessionContactCount} - {sessionContactCount === 1 ? 'Kontakt' : 'Kontakte'} lokal gespeichert - {:else} - - Kontakte werden nur in diesem Tab gespeichert - {/if} + Demo-Modus +