From 4681ba8c364c80cac366c4aca41384b1fe77c41c Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:30:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20create=20creat?= =?UTF-8?q?eAppSettingsStore=20factory=20and=20migrate=203=20apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add createAppSettingsStore() factory to @manacore/shared-stores - Migrate todo, contacts, calendar settings stores to use factory - Factory provides: localStorage persistence, type-safe set/update/reset - Optional onSettingsChange callback for cloud sync (used by calendar) - Reduces boilerplate by ~323 LOC across 3 apps Savings: - todo: 259 → 159 LOC (100 LOC) - contacts: 278 → 173 LOC (105 LOC) - calendar: 433 → 315 LOC (118 LOC) --- apps/calendar/apps/web/package.json | 1 + .../web/src/lib/stores/settings.svelte.ts | 325 ++++++------------ apps/contacts/apps/web/package.json | 1 + .../web/src/lib/stores/settings.svelte.ts | 180 +++------- apps/todo/apps/web/package.json | 1 + .../web/src/lib/stores/settings.svelte.ts | 169 ++------- docs/CONSOLIDATION_OPPORTUNITIES.md | 74 ++-- packages/shared-stores/src/index.ts | 5 + packages/shared-stores/src/settings.svelte.ts | 149 ++++++++ 9 files changed, 361 insertions(+), 544 deletions(-) create mode 100644 packages/shared-stores/src/settings.svelte.ts diff --git a/apps/calendar/apps/web/package.json b/apps/calendar/apps/web/package.json index 63d9acda3..80dfc7e73 100644 --- a/apps/calendar/apps/web/package.json +++ b/apps/calendar/apps/web/package.json @@ -35,6 +35,7 @@ "@manacore/shared-api-client": "workspace:*", "@manacore/shared-auth": "workspace:*", "@manacore/shared-splitscreen": "workspace:*", + "@manacore/shared-stores": "workspace:*", "@manacore/shared-auth-ui": "workspace:*", "@manacore/shared-branding": "workspace:*", "@manacore/shared-feedback-service": "workspace:*", diff --git a/apps/calendar/apps/web/src/lib/stores/settings.svelte.ts b/apps/calendar/apps/web/src/lib/stores/settings.svelte.ts index b82b4b1d3..5a3f9f50b 100644 --- a/apps/calendar/apps/web/src/lib/stores/settings.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/settings.svelte.ts @@ -1,20 +1,19 @@ /** * Settings Store - Manages user preferences for the calendar app - * Uses Svelte 5 runes with: - * - localStorage for immediate persistence - * - userSettings store for cloud sync (device-specific) + * Uses @manacore/shared-stores createAppSettingsStore factory with cloud sync */ import { browser } from '$app/environment'; import type { CalendarViewType } from '@calendar/shared'; +import { createAppSettingsStore } from '@manacore/shared-stores'; import { userSettings } from './user-settings.svelte'; // Settings types -export type WeekStartDay = 0 | 1; // 0 = Sunday, 1 = Monday +export type WeekStartDay = 0 | 1; export type TimeFormat = '24h' | '12h'; -export type AllDayDisplayMode = 'header' | 'block'; // header = separate row, block = full day block in grid +export type AllDayDisplayMode = 'header' | 'block'; export type WeekdayFormat = 'full' | 'short' | 'hidden'; -export type SttLanguage = 'de' | 'auto'; // Speech-to-text language setting +export type SttLanguage = 'de' | 'auto'; export interface CalendarAppSettings { // View settings @@ -23,56 +22,56 @@ export interface CalendarAppSettings { showOnlyWeekdays: boolean; showWeekNumbers: boolean; timeFormat: TimeFormat; - filterHoursEnabled: boolean; // Filter visible hours - dayStartHour: number; // First visible hour (0-23) - dayEndHour: number; // Last visible hour (0-23) - allDayDisplayMode: AllDayDisplayMode; // How to display all-day events + filterHoursEnabled: boolean; + dayStartHour: number; + dayEndHour: number; + allDayDisplayMode: AllDayDisplayMode; // Header settings - headerCompact: boolean; // Compact header display - headerWeekdayFormat: WeekdayFormat; // Weekday display format - headerShowDate: boolean; // Show date in header - headerAlwaysShowMonth: boolean; // Always show month (e.g., "13.12.") + headerCompact: boolean; + headerWeekdayFormat: WeekdayFormat; + headerShowDate: boolean; + headerAlwaysShowMonth: boolean; // DateStrip settings - dateStripShowMoonPhases: boolean; // Show moon phase indicators - dateStripShowEventIndicators: boolean; // Show event dot indicators - dateStripShowWeekday: boolean; // Show weekday names (Mo, Di, Mi...) - dateStripHighlightWeekends: boolean; // Visually highlight weekend days - dateStripShowMonthDividers: boolean; // Show vertical dividers between months - dateStripCompact: boolean; // Use compact/smaller DateStrip - dateStripShowWeekNumbers: boolean; // Show week numbers at start of week - dateStripCollapsed: boolean; // Whether DateStrip is minimized to FAB + dateStripShowMoonPhases: boolean; + dateStripShowEventIndicators: boolean; + dateStripShowWeekday: boolean; + dateStripHighlightWeekends: boolean; + dateStripShowMonthDividers: boolean; + dateStripCompact: boolean; + dateStripShowWeekNumbers: boolean; + dateStripCollapsed: boolean; // TagStrip settings - tagStripCollapsed: boolean; // Whether TagStrip is hidden - selectedTagIds: string[]; // Tags selected for filtering calendar view + tagStripCollapsed: boolean; + selectedTagIds: string[]; // Immersive Mode settings - immersiveModeEnabled: boolean; // Fullscreen mode - hides all UI elements + immersiveModeEnabled: boolean; - // Birthday settings (cross-app integration with Contacts) - showBirthdays: boolean; // Show contact birthdays in calendar - showBirthdayAge: boolean; // Show age in birthday events + // Birthday settings + showBirthdays: boolean; + showBirthdayAge: boolean; // UI settings sidebarCollapsed: boolean; // Quick View Pill settings - quickViewPillViews: CalendarViewType[]; // Views shown in quick switcher - customDayCount: number; // Custom day count for 'custom' view type (1-365) + quickViewPillViews: CalendarViewType[]; + customDayCount: number; // Event defaults - defaultEventDuration: number; // in minutes - defaultReminder: number; // in minutes before event + defaultEventDuration: number; + defaultReminder: number; // Voice input settings - sttLanguage: SttLanguage; // Speech-to-text language ('de' or 'auto') + sttLanguage: SttLanguage; } const DEFAULT_SETTINGS: CalendarAppSettings = { defaultView: 'week', - weekStartsOn: 1, // Monday + weekStartsOn: 1, showOnlyWeekdays: false, showWeekNumbers: false, timeFormat: '24h', @@ -80,12 +79,10 @@ const DEFAULT_SETTINGS: CalendarAppSettings = { dayStartHour: 6, dayEndHour: 20, allDayDisplayMode: 'header', - // Header defaults headerCompact: false, headerWeekdayFormat: 'full', headerShowDate: true, headerAlwaysShowMonth: false, - // DateStrip defaults dateStripShowMoonPhases: true, dateStripShowEventIndicators: true, dateStripShowWeekday: true, @@ -94,67 +91,26 @@ const DEFAULT_SETTINGS: CalendarAppSettings = { dateStripCompact: false, dateStripShowWeekNumbers: false, dateStripCollapsed: false, - // TagStrip defaults - tagStripCollapsed: true, // Hidden by default - selectedTagIds: [], // No tags selected by default - // Immersive Mode defaults + tagStripCollapsed: true, + selectedTagIds: [], immersiveModeEnabled: false, - // Birthday defaults showBirthdays: true, showBirthdayAge: true, - // UI defaults sidebarCollapsed: false, - // Quick View Pill defaults quickViewPillViews: ['week', 'month', 'agenda'], - customDayCount: 30, // Default: 30 days (1 month) - // Event defaults + customDayCount: 30, defaultEventDuration: 60, defaultReminder: 15, - // Voice input defaults sttLanguage: 'de', }; -const STORAGE_KEY = 'calendar-settings'; - -// Load settings from localStorage -function loadSettings(): CalendarAppSettings { - if (!browser) return DEFAULT_SETTINGS; - - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - return { ...DEFAULT_SETTINGS, ...parsed }; - } - } catch (e) { - console.error('Failed to load calendar settings:', e); - } - - return DEFAULT_SETTINGS; -} - -// Save settings to localStorage -function saveSettings(settings: CalendarAppSettings) { - if (!browser) return; - - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); - } catch (e) { - console.error('Failed to save calendar settings:', e); - } -} - -// State -let settings = $state(loadSettings()); +// Cloud sync state let cloudSyncEnabled = $state(false); let initialSyncDone = $state(false); -/** - * Sync settings to cloud (device-specific) - */ -async function syncToCloud() { +// Sync to cloud callback +async function syncToCloud(settings: CalendarAppSettings) { if (!cloudSyncEnabled || !browser) return; - try { await userSettings.updateDeviceAppSettings(settings as unknown as Record); } catch (e) { @@ -162,12 +118,18 @@ async function syncToCloud() { } } -/** - * Load settings from cloud (device-specific) - */ +// Create base store with cloud sync callback +const baseStore = createAppSettingsStore( + 'calendar-settings', + DEFAULT_SETTINGS, + { + onSettingsChange: syncToCloud, + } +); + +// Load settings from cloud function loadFromCloud(): Partial | null { if (!userSettings.loaded) return null; - const cloudSettings = userSettings.currentDeviceAppSettings; if (cloudSettings && Object.keys(cloudSettings).length > 0) { return cloudSettings as unknown as Partial; @@ -176,240 +138,166 @@ function loadFromCloud(): Partial | null { } export const settingsStore = { - // Getters + // Base store methods get settings() { - return settings; + return baseStore.settings; }, + initialize: baseStore.initialize, + set: baseStore.set, + update: baseStore.update, + reset: baseStore.reset, + getDefaults: baseStore.getDefaults, + toggleImmersiveMode: baseStore.toggleImmersiveMode, + + // Convenience getters get defaultView() { - return settings.defaultView; + return baseStore.settings.defaultView; }, get weekStartsOn() { - return settings.weekStartsOn; + return baseStore.settings.weekStartsOn; }, get showOnlyWeekdays() { - return settings.showOnlyWeekdays; + return baseStore.settings.showOnlyWeekdays; }, get showWeekNumbers() { - return settings.showWeekNumbers; + return baseStore.settings.showWeekNumbers; }, get timeFormat() { - return settings.timeFormat; + return baseStore.settings.timeFormat; }, get filterHoursEnabled() { - return settings.filterHoursEnabled; + return baseStore.settings.filterHoursEnabled; }, get dayStartHour() { - return settings.dayStartHour; + return baseStore.settings.dayStartHour; }, get dayEndHour() { - return settings.dayEndHour; + return baseStore.settings.dayEndHour; }, get allDayDisplayMode() { - return settings.allDayDisplayMode; + return baseStore.settings.allDayDisplayMode; }, - // Header settings get headerCompact() { - return settings.headerCompact; + return baseStore.settings.headerCompact; }, get headerWeekdayFormat() { - return settings.headerWeekdayFormat; + return baseStore.settings.headerWeekdayFormat; }, get headerShowDate() { - return settings.headerShowDate; + return baseStore.settings.headerShowDate; }, get headerAlwaysShowMonth() { - return settings.headerAlwaysShowMonth; + return baseStore.settings.headerAlwaysShowMonth; }, - // DateStrip settings get dateStripShowMoonPhases() { - return settings.dateStripShowMoonPhases; + return baseStore.settings.dateStripShowMoonPhases; }, get dateStripShowEventIndicators() { - return settings.dateStripShowEventIndicators; + return baseStore.settings.dateStripShowEventIndicators; }, get dateStripShowWeekday() { - return settings.dateStripShowWeekday; + return baseStore.settings.dateStripShowWeekday; }, get dateStripHighlightWeekends() { - return settings.dateStripHighlightWeekends; + return baseStore.settings.dateStripHighlightWeekends; }, get dateStripShowMonthDividers() { - return settings.dateStripShowMonthDividers; + return baseStore.settings.dateStripShowMonthDividers; }, get dateStripCompact() { - return settings.dateStripCompact; + return baseStore.settings.dateStripCompact; }, get dateStripShowWeekNumbers() { - return settings.dateStripShowWeekNumbers; + return baseStore.settings.dateStripShowWeekNumbers; }, get dateStripCollapsed() { - return settings.dateStripCollapsed; + return baseStore.settings.dateStripCollapsed; }, - // TagStrip settings get tagStripCollapsed() { - return settings.tagStripCollapsed; + return baseStore.settings.tagStripCollapsed; }, get selectedTagIds() { - return settings.selectedTagIds; + return baseStore.settings.selectedTagIds; }, get hasSelectedTags() { - return settings.selectedTagIds.length > 0; + return baseStore.settings.selectedTagIds.length > 0; }, - // Immersive Mode settings get immersiveModeEnabled() { - return settings.immersiveModeEnabled; + return baseStore.settings.immersiveModeEnabled; }, - // Birthday settings get showBirthdays() { - return settings.showBirthdays; + return baseStore.settings.showBirthdays; }, get showBirthdayAge() { - return settings.showBirthdayAge; + return baseStore.settings.showBirthdayAge; }, get defaultEventDuration() { - return settings.defaultEventDuration; + return baseStore.settings.defaultEventDuration; }, get defaultReminder() { - return settings.defaultReminder; + return baseStore.settings.defaultReminder; }, get sidebarCollapsed() { - return settings.sidebarCollapsed; + return baseStore.settings.sidebarCollapsed; }, get quickViewPillViews() { - return settings.quickViewPillViews; + return baseStore.settings.quickViewPillViews; }, get customDayCount() { - return settings.customDayCount; + return baseStore.settings.customDayCount; }, get sttLanguage() { - return settings.sttLanguage; + return baseStore.settings.sttLanguage; }, get cloudSyncEnabled() { return cloudSyncEnabled; }, - /** - * Enable cloud sync and load settings from cloud - */ + // Cloud sync methods enableCloudSync() { cloudSyncEnabled = true; - - // On first sync, prefer cloud settings over local if they exist if (!initialSyncDone) { const cloudSettings = loadFromCloud(); if (cloudSettings && Object.keys(cloudSettings).length > 0) { - settings = { ...DEFAULT_SETTINGS, ...settings, ...cloudSettings }; - saveSettings(settings); + baseStore.update(cloudSettings); } else { - // No cloud settings yet, push local settings to cloud - syncToCloud(); + syncToCloud(baseStore.settings); } initialSyncDone = true; } }, - /** - * Disable cloud sync - */ disableCloudSync() { cloudSyncEnabled = false; }, - /** - * Toggle sidebar collapsed state - */ + // Calendar-specific toggle methods toggleSidebar() { - settings = { ...settings, sidebarCollapsed: !settings.sidebarCollapsed }; - saveSettings(settings); - syncToCloud(); + baseStore.set('sidebarCollapsed', !baseStore.settings.sidebarCollapsed); }, - /** - * Toggle TagStrip visibility - */ toggleTagStrip() { - settings = { ...settings, tagStripCollapsed: !settings.tagStripCollapsed }; - saveSettings(settings); - syncToCloud(); + baseStore.set('tagStripCollapsed', !baseStore.settings.tagStripCollapsed); }, - /** - * Toggle a tag selection for filtering - */ toggleTagSelection(tagId: string) { - const currentIds = settings.selectedTagIds; + const currentIds = baseStore.settings.selectedTagIds; const isSelected = currentIds.includes(tagId); const newIds = isSelected ? currentIds.filter((id) => id !== tagId) : [...currentIds, tagId]; - settings = { ...settings, selectedTagIds: newIds }; - saveSettings(settings); - syncToCloud(); + baseStore.set('selectedTagIds', newIds); }, - /** - * Check if a tag is selected - */ isTagSelected(tagId: string): boolean { - return settings.selectedTagIds.includes(tagId); + return baseStore.settings.selectedTagIds.includes(tagId); }, - /** - * Clear all tag selections - */ clearTagSelection() { - settings = { ...settings, selectedTagIds: [] }; - saveSettings(settings); - syncToCloud(); + baseStore.set('selectedTagIds', []); }, - /** - * Toggle Immersive Mode (fullscreen, hide all UI) - */ - toggleImmersiveMode() { - settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled }; - saveSettings(settings); - syncToCloud(); - }, - - /** - * Initialize settings from localStorage - */ - initialize() { - if (!browser) return; - settings = loadSettings(); - }, - - /** - * Update a single setting - */ - set(key: K, value: CalendarAppSettings[K]) { - settings = { ...settings, [key]: value }; - saveSettings(settings); - syncToCloud(); - }, - - /** - * Update multiple settings at once - */ - update(updates: Partial) { - settings = { ...settings, ...updates }; - saveSettings(settings); - syncToCloud(); - }, - - /** - * Reset all settings to defaults - */ - reset() { - settings = { ...DEFAULT_SETTINGS }; - saveSettings(settings); - syncToCloud(); - }, - - /** - * Format time according to user preference - */ + // Time formatting helpers formatTime(date: Date): string { - if (settings.timeFormat === '12h') { + if (baseStore.settings.timeFormat === '12h') { const hours = date.getHours(); const minutes = date.getMinutes(); const ampm = hours >= 12 ? 'PM' : 'AM'; @@ -419,11 +307,8 @@ export const settingsStore = { return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; }, - /** - * Format hour label according to user preference - */ formatHour(hour: number): string { - if (settings.timeFormat === '12h') { + if (baseStore.settings.timeFormat === '12h') { const ampm = hour >= 12 ? 'PM' : 'AM'; const displayHour = hour % 12 || 12; return `${displayHour} ${ampm}`; diff --git a/apps/contacts/apps/web/package.json b/apps/contacts/apps/web/package.json index 578b0bcbe..58a2e2422 100644 --- a/apps/contacts/apps/web/package.json +++ b/apps/contacts/apps/web/package.json @@ -43,6 +43,7 @@ "@manacore/shared-icons": "workspace:*", "@manacore/shared-profile-ui": "workspace:*", "@manacore/shared-splitscreen": "workspace:*", + "@manacore/shared-stores": "workspace:*", "@manacore/shared-subscription-ui": "workspace:*", "@manacore/shared-tags": "workspace:*", "@manacore/shared-tailwind": "workspace:*", diff --git a/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts b/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts index 21a036ab8..39c23db31 100644 --- a/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/settings.svelte.ts @@ -1,9 +1,9 @@ /** * Settings Store - Manages user preferences for the Contacts app - * Uses Svelte 5 runes and localStorage for persistence + * Uses @manacore/shared-stores createAppSettingsStore factory */ -import { browser } from '$app/environment'; +import { createAppSettingsStore } from '@manacore/shared-stores'; // Settings types export type ContactSortBy = 'name' | 'company' | 'created' | 'updated'; @@ -13,61 +13,39 @@ export type DateFormat = 'dd.MM.yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd'; export interface ContactsAppSettings { // Display Settings - /** Default view mode for contacts list */ defaultView: ContactView; - /** Default sort field */ sortBy: ContactSortBy; - /** Default sort order */ sortOrder: ContactSortOrder; - /** Show contact photos in list */ showPhotos: boolean; - /** Show company name in list */ showCompany: boolean; - /** Contacts per page in list view */ contactsPerPage: number; // Contact Display - /** Display name format: 'first-last' or 'last-first' */ nameFormat: 'first-last' | 'last-first'; - /** Date format for birthdays etc. */ dateFormat: DateFormat; - /** Show birthday reminders */ showBirthdayReminders: boolean; - /** Days before birthday to remind */ birthdayReminderDays: number; // Import/Export - /** Default export format */ defaultExportFormat: 'vcf' | 'csv' | 'json'; - /** Include notes in export */ includeNotesInExport: boolean; - /** Include photos in export */ includePhotosInExport: boolean; // Duplicates - /** Auto-detect duplicates on import */ autoDetectDuplicates: boolean; - /** Duplicate detection sensitivity: 'strict' | 'normal' | 'loose' */ duplicateSensitivity: 'strict' | 'normal' | 'loose'; // Privacy - /** Blur contact photos by default (privacy mode) */ privacyMode: boolean; - /** Require confirmation before sharing contact */ confirmBeforeSharing: boolean; // Alphabet Navigation Settings - /** Hide letters that have no contacts */ alphabetNavHideInactive: boolean; - /** Use compact/smaller alphabet buttons */ alphabetNavCompact: boolean; - /** Reverse letter order (Z-A instead of A-Z) */ alphabetNavReverseOrder: boolean; - /** Show # symbol for non-letter names */ alphabetNavShowHash: boolean; // Immersive Mode - /** Fullscreen mode - hides all UI elements */ immersiveModeEnabled: boolean; } @@ -109,170 +87,90 @@ const DEFAULT_SETTINGS: ContactsAppSettings = { immersiveModeEnabled: false, }; -const STORAGE_KEY = 'contacts-settings'; - -// Load settings from localStorage -function loadSettings(): ContactsAppSettings { - if (!browser) return DEFAULT_SETTINGS; - - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - // Merge with defaults to handle new settings added in updates - return { ...DEFAULT_SETTINGS, ...parsed }; - } - } catch (e) { - console.error('Failed to load contacts settings:', e); - } - - return DEFAULT_SETTINGS; -} - -// Save settings to localStorage -function saveSettings(settings: ContactsAppSettings) { - if (!browser) return; - - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); - } catch (e) { - console.error('Failed to save contacts settings:', e); - } -} - -// State -let settings = $state(loadSettings()); +// Create base store using factory +const baseStore = createAppSettingsStore( + 'contacts-settings', + DEFAULT_SETTINGS +); +// Export with convenience getters for backwards compatibility export const contactsSettings = { - // Full settings object + // Base store methods get settings() { - return settings; + return baseStore.settings; }, + initialize: baseStore.initialize, + set: baseStore.set, + update: baseStore.update, + reset: baseStore.reset, + getDefaults: baseStore.getDefaults, + toggleImmersiveMode: baseStore.toggleImmersiveMode, - // Display Settings + // Convenience getters (backwards compatible) get defaultView() { - return settings.defaultView; + return baseStore.settings.defaultView; }, get sortBy() { - return settings.sortBy; + return baseStore.settings.sortBy; }, get sortOrder() { - return settings.sortOrder; + return baseStore.settings.sortOrder; }, get showPhotos() { - return settings.showPhotos; + return baseStore.settings.showPhotos; }, get showCompany() { - return settings.showCompany; + return baseStore.settings.showCompany; }, get contactsPerPage() { - return settings.contactsPerPage; + return baseStore.settings.contactsPerPage; }, - - // Contact Display get nameFormat() { - return settings.nameFormat; + return baseStore.settings.nameFormat; }, get dateFormat() { - return settings.dateFormat; + return baseStore.settings.dateFormat; }, get showBirthdayReminders() { - return settings.showBirthdayReminders; + return baseStore.settings.showBirthdayReminders; }, get birthdayReminderDays() { - return settings.birthdayReminderDays; + return baseStore.settings.birthdayReminderDays; }, - - // Import/Export get defaultExportFormat() { - return settings.defaultExportFormat; + return baseStore.settings.defaultExportFormat; }, get includeNotesInExport() { - return settings.includeNotesInExport; + return baseStore.settings.includeNotesInExport; }, get includePhotosInExport() { - return settings.includePhotosInExport; + return baseStore.settings.includePhotosInExport; }, - - // Duplicates get autoDetectDuplicates() { - return settings.autoDetectDuplicates; + return baseStore.settings.autoDetectDuplicates; }, get duplicateSensitivity() { - return settings.duplicateSensitivity; + return baseStore.settings.duplicateSensitivity; }, - - // Privacy get privacyMode() { - return settings.privacyMode; + return baseStore.settings.privacyMode; }, get confirmBeforeSharing() { - return settings.confirmBeforeSharing; + return baseStore.settings.confirmBeforeSharing; }, - - // Alphabet Navigation get alphabetNavHideInactive() { - return settings.alphabetNavHideInactive; + return baseStore.settings.alphabetNavHideInactive; }, get alphabetNavCompact() { - return settings.alphabetNavCompact; + return baseStore.settings.alphabetNavCompact; }, get alphabetNavReverseOrder() { - return settings.alphabetNavReverseOrder; + return baseStore.settings.alphabetNavReverseOrder; }, get alphabetNavShowHash() { - return settings.alphabetNavShowHash; + return baseStore.settings.alphabetNavShowHash; }, - - // Immersive Mode get immersiveModeEnabled() { - return settings.immersiveModeEnabled; - }, - - /** - * Toggle Immersive Mode (fullscreen, hide all UI) - */ - toggleImmersiveMode() { - settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled }; - saveSettings(settings); - }, - - /** - * Initialize settings from localStorage - */ - initialize() { - if (!browser) return; - settings = loadSettings(); - }, - - /** - * Update a single setting - */ - set(key: K, value: ContactsAppSettings[K]) { - settings = { ...settings, [key]: value }; - saveSettings(settings); - }, - - /** - * Update multiple settings at once - */ - update(updates: Partial) { - settings = { ...settings, ...updates }; - saveSettings(settings); - }, - - /** - * Reset all settings to defaults - */ - reset() { - settings = { ...DEFAULT_SETTINGS }; - saveSettings(settings); - }, - - /** - * Get default settings (for reference) - */ - getDefaults() { - return DEFAULT_SETTINGS; + return baseStore.settings.immersiveModeEnabled; }, }; diff --git a/apps/todo/apps/web/package.json b/apps/todo/apps/web/package.json index ff4b7f56a..cb7854a84 100644 --- a/apps/todo/apps/web/package.json +++ b/apps/todo/apps/web/package.json @@ -33,6 +33,7 @@ "@manacore/shared-api-client": "workspace:*", "@manacore/shared-auth": "workspace:*", "@manacore/shared-splitscreen": "workspace:*", + "@manacore/shared-stores": "workspace:*", "@manacore/shared-types": "workspace:*", "@manacore/shared-utils": "workspace:*", "@manacore/shared-tags": "workspace:*", diff --git a/apps/todo/apps/web/src/lib/stores/settings.svelte.ts b/apps/todo/apps/web/src/lib/stores/settings.svelte.ts index da510d5e3..d8c1efac5 100644 --- a/apps/todo/apps/web/src/lib/stores/settings.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/settings.svelte.ts @@ -1,9 +1,9 @@ /** * Settings Store - Manages user preferences for the Todo app - * Uses Svelte 5 runes and localStorage for persistence + * Uses @manacore/shared-stores createAppSettingsStore factory */ -import { browser } from '$app/environment'; +import { createAppSettingsStore } from '@manacore/shared-stores'; import type { TaskPriority } from '@todo/shared'; // Settings types @@ -12,55 +12,35 @@ export type KanbanCardSize = 'compact' | 'normal' | 'large'; export interface TodoAppSettings { // Task Behavior - /** Default priority for new tasks */ defaultPriority: TaskPriority; - /** Default due time for tasks (HH:mm format, null = no default) */ defaultDueTime: string | null; - /** Auto-archive completed tasks after X days (null = disabled) */ autoArchiveCompletedDays: number | null; - /** Default project for quick add (null = inbox) */ quickAddProject: string | null; // View & Display - /** Default view when opening the app */ defaultView: TodoView; - /** Show task counts as badges in navigation */ showTaskCounts: boolean; - /** Compact mode with reduced padding */ compactMode: boolean; - /** Show progress bar for subtasks */ showSubtaskProgress: boolean; - /** Group tasks by project in list views */ groupByProject: boolean; // Kanban Board - /** Kanban card size */ kanbanCardSize: KanbanCardSize; - /** Show labels on kanban cards */ showLabelsOnCards: boolean; - /** Work-in-progress limit per column (null = unlimited) */ wipLimitPerColumn: number | null; // Notifications & Reminders - /** Default reminder time in minutes before due (null = no default) */ defaultReminderMinutes: number | null; - /** Enable daily digest email/notification */ dailyDigestEnabled: boolean; - /** Notify about overdue tasks */ overdueNotifications: boolean; // Productivity - /** Focus mode - show only current task */ focusMode: boolean; - /** Enable pomodoro timer */ pomodoroEnabled: boolean; - /** Daily task completion goal (null = no goal) */ dailyGoal: number | null; - /** Show productivity streak */ showStreak: boolean; // Immersive Mode - /** Fullscreen mode - hides all UI elements */ immersiveModeEnabled: boolean; } @@ -98,162 +78,81 @@ const DEFAULT_SETTINGS: TodoAppSettings = { immersiveModeEnabled: false, }; -const STORAGE_KEY = 'todo-settings'; - -// Load settings from localStorage -function loadSettings(): TodoAppSettings { - if (!browser) return DEFAULT_SETTINGS; - - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - // Merge with defaults to handle new settings added in updates - return { ...DEFAULT_SETTINGS, ...parsed }; - } - } catch (e) { - console.error('Failed to load todo settings:', e); - } - - return DEFAULT_SETTINGS; -} - -// Save settings to localStorage -function saveSettings(settings: TodoAppSettings) { - if (!browser) return; - - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); - } catch (e) { - console.error('Failed to save todo settings:', e); - } -} - -// State -let settings = $state(loadSettings()); +// Create base store using factory +const baseStore = createAppSettingsStore('todo-settings', DEFAULT_SETTINGS); +// Export with convenience getters for backwards compatibility export const todoSettings = { - // Full settings object + // Base store methods get settings() { - return settings; + return baseStore.settings; }, + initialize: baseStore.initialize, + set: baseStore.set, + update: baseStore.update, + reset: baseStore.reset, + getDefaults: baseStore.getDefaults, + toggleImmersiveMode: baseStore.toggleImmersiveMode, - // Task Behavior + // Convenience getters (backwards compatible) get defaultPriority() { - return settings.defaultPriority; + return baseStore.settings.defaultPriority; }, get defaultDueTime() { - return settings.defaultDueTime; + return baseStore.settings.defaultDueTime; }, get autoArchiveCompletedDays() { - return settings.autoArchiveCompletedDays; + return baseStore.settings.autoArchiveCompletedDays; }, get quickAddProject() { - return settings.quickAddProject; + return baseStore.settings.quickAddProject; }, - - // View & Display get defaultView() { - return settings.defaultView; + return baseStore.settings.defaultView; }, get showTaskCounts() { - return settings.showTaskCounts; + return baseStore.settings.showTaskCounts; }, get compactMode() { - return settings.compactMode; + return baseStore.settings.compactMode; }, get showSubtaskProgress() { - return settings.showSubtaskProgress; + return baseStore.settings.showSubtaskProgress; }, get groupByProject() { - return settings.groupByProject; + return baseStore.settings.groupByProject; }, - - // Kanban Board get kanbanCardSize() { - return settings.kanbanCardSize; + return baseStore.settings.kanbanCardSize; }, get showLabelsOnCards() { - return settings.showLabelsOnCards; + return baseStore.settings.showLabelsOnCards; }, get wipLimitPerColumn() { - return settings.wipLimitPerColumn; + return baseStore.settings.wipLimitPerColumn; }, - - // Notifications & Reminders get defaultReminderMinutes() { - return settings.defaultReminderMinutes; + return baseStore.settings.defaultReminderMinutes; }, get dailyDigestEnabled() { - return settings.dailyDigestEnabled; + return baseStore.settings.dailyDigestEnabled; }, get overdueNotifications() { - return settings.overdueNotifications; + return baseStore.settings.overdueNotifications; }, - - // Productivity get focusMode() { - return settings.focusMode; + return baseStore.settings.focusMode; }, get pomodoroEnabled() { - return settings.pomodoroEnabled; + return baseStore.settings.pomodoroEnabled; }, get dailyGoal() { - return settings.dailyGoal; + return baseStore.settings.dailyGoal; }, get showStreak() { - return settings.showStreak; + return baseStore.settings.showStreak; }, - - // Immersive Mode get immersiveModeEnabled() { - return settings.immersiveModeEnabled; - }, - - /** - * Toggle Immersive Mode (fullscreen, hide all UI) - */ - toggleImmersiveMode() { - settings = { ...settings, immersiveModeEnabled: !settings.immersiveModeEnabled }; - saveSettings(settings); - }, - - /** - * Initialize settings from localStorage - */ - initialize() { - if (!browser) return; - settings = loadSettings(); - }, - - /** - * Update a single setting - */ - set(key: K, value: TodoAppSettings[K]) { - settings = { ...settings, [key]: value }; - saveSettings(settings); - }, - - /** - * Update multiple settings at once - */ - update(updates: Partial) { - settings = { ...settings, ...updates }; - saveSettings(settings); - }, - - /** - * Reset all settings to defaults - */ - reset() { - settings = { ...DEFAULT_SETTINGS }; - saveSettings(settings); - }, - - /** - * Get default settings (for reference) - */ - getDefaults() { - return DEFAULT_SETTINGS; + return baseStore.settings.immersiveModeEnabled; }, }; diff --git a/docs/CONSOLIDATION_OPPORTUNITIES.md b/docs/CONSOLIDATION_OPPORTUNITIES.md index 5c1097e14..818e2b6aa 100644 --- a/docs/CONSOLIDATION_OPPORTUNITIES.md +++ b/docs/CONSOLIDATION_OPPORTUNITIES.md @@ -9,7 +9,7 @@ |-----------|---------|----------------------|---------| | ~~**KRITISCH**~~ | ~~Backend Metrics Migration~~ | ~~350 LOC~~ ✅ **709 LOC entfernt** | ~~Niedrig~~ | | **HOCH** | Skeleton Components | 800-1.000 LOC | Mittel | -| **HOCH** | App Settings Stores | 600-700 LOC | Mittel | +| ~~**HOCH**~~ | ~~App Settings Stores~~ | ~~600-700 LOC~~ ✅ **323 LOC entfernt** | ~~Mittel~~ | | **HOCH** | Main.ts/CORS Patterns | 1.800 LOC | Mittel | | **MITTEL** | TypeScript Configs | 400 LOC | Niedrig | | **MITTEL** | UI Component Cleanup | 400 LOC | Niedrig | @@ -146,54 +146,32 @@ import { HealthModule } from '@manacore/shared-nestjs-health'; ## 2. Frontend Stores (Svelte 5) -### 2.1 HOCH: App Settings Stores (600-700 LOC) +### ~~2.1 HOCH: App Settings Stores~~ ✅ ERLEDIGT (323 LOC gespart) -**Problem:** 3 Apps (todo, calendar, contacts) haben fast identische Settings-Store Implementierungen mit localStorage Persistenz. +**Status:** `createAppSettingsStore()` Factory erstellt und 3 Apps migriert (29.01.2026) -**Betroffene Dateien:** -- `apps/todo/apps/web/src/lib/stores/settings.svelte.ts` (259 LOC) -- `apps/calendar/apps/web/src/lib/stores/settings.svelte.ts` (433 LOC) -- `apps/contacts/apps/web/src/lib/stores/settings.svelte.ts` (278 LOC) +**Erstellte Factory:** `packages/shared-stores/src/settings.svelte.ts` +- Type-safe Settings Store mit localStorage Persistenz +- Optional: `onSettingsChange` Callback für Cloud-Sync +- Reduziert Boilerplate von ~100 LOC pro App auf ~20 LOC -**Dupliziertes Pattern (100% identisch):** -```typescript -// Boilerplate in jedem (80-100 LOC): -- TypeScript Interface für Settings -- DEFAULT_SETTINGS Konstante -- STORAGE_KEY -- loadSettings() - localStorage laden + merge mit defaults -- saveSettings() - localStorage speichern -- let settings = $state(...) -- toggleImmersiveMode(), initialize(), set(), update(), reset(), getDefaults() -``` - -**Empfehlung:** Erstelle `createAppSettingsStore()` Factory in `@manacore/shared-stores` +**Migrierte Apps:** +- ~~`apps/todo/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (259 → 159 LOC = 100 LOC) +- ~~`apps/contacts/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (278 → 173 LOC = 105 LOC) +- ~~`apps/calendar/apps/web/src/lib/stores/settings.svelte.ts`~~ ✅ (433 → 315 LOC = 118 LOC) ```typescript -// packages/shared-stores/src/createAppSettingsStore.ts -export function createAppSettingsStore>( - storageKey: string, - defaultSettings: T, - options?: { cloudSync?: boolean } -) { - let settings = $state(defaultSettings); - - function loadSettings(): T { /* localStorage logic */ } - function saveSettings(newSettings: T): void { /* localStorage logic */ } - - return { - get value() { return settings; }, - initialize() { settings = loadSettings(); }, - set(key: K, value: T[K]) { /* ... */ }, - update(updates: Partial) { /* ... */ }, - reset() { settings = defaultSettings; saveSettings(settings); }, - getDefaults() { return defaultSettings; }, - }; -} +// Nachher (Beispiel Todo) +import { createAppSettingsStore } from '@manacore/shared-stores'; +const baseStore = createAppSettingsStore('todo-settings', DEFAULT_SETTINGS); +export const todoSettings = { + get settings() { return baseStore.settings; }, + initialize: baseStore.initialize, + set: baseStore.set, + // ... convenience getters +}; ``` -**Einsparung:** ~200 LOC Boilerplate pro App = 600 LOC - --- ### 2.2 MITTEL: Navigation Stores (50 LOC) @@ -409,12 +387,12 @@ export default createDrizzleConfig('chat'); ### Phase 2: Stores & Configs (3-5 Tage, ~1.500 LOC) -| Aufgabe | LOC | Aufwand | -|---------|-----|---------| -| `createAppSettingsStore()` Factory erstellen | 600 | Mittel | -| `@manacore/shared-tsconfig` Package erstellen | 400 | Niedrig | -| `@manacore/shared-vite-config` Factory erstellen | 300 | Niedrig | -| Navigation Store Factory erstellen | 50 | Niedrig | +| Aufgabe | LOC | Aufwand | Status | +|---------|-----|---------|--------| +| ~~`createAppSettingsStore()` Factory erstellen~~ | ~~600~~ → **323** | ~~Mittel~~ | ✅ Erledigt | +| `@manacore/shared-tsconfig` Package erstellen | 400 | Niedrig | Offen | +| `@manacore/shared-vite-config` Factory erstellen | 300 | Niedrig | Offen | +| Navigation Store Factory erstellen | 50 | Niedrig | Offen | ### Phase 3: Backend Setup (5-7 Tage, ~2.000 LOC) diff --git a/packages/shared-stores/src/index.ts b/packages/shared-stores/src/index.ts index 46c334d50..557dc07cf 100644 --- a/packages/shared-stores/src/index.ts +++ b/packages/shared-stores/src/index.ts @@ -10,3 +10,8 @@ export { type NavigationStore, } from './navigation.svelte'; export { createThemeStore, type ThemeStore, type ThemeMode } from './theme.svelte'; +export { + createAppSettingsStore, + type AppSettingsStore, + type AppSettingsStoreOptions, +} from './settings.svelte'; diff --git a/packages/shared-stores/src/settings.svelte.ts b/packages/shared-stores/src/settings.svelte.ts new file mode 100644 index 000000000..6a96b217c --- /dev/null +++ b/packages/shared-stores/src/settings.svelte.ts @@ -0,0 +1,149 @@ +/** + * App Settings Store Factory + * + * Creates a type-safe settings store with localStorage persistence. + * Reduces ~200 LOC boilerplate per app to ~20 LOC. + * + * @example + * ```typescript + * interface MyAppSettings { + * theme: 'light' | 'dark'; + * sidebarCollapsed: boolean; + * immersiveModeEnabled: boolean; + * } + * + * const DEFAULT_SETTINGS: MyAppSettings = { + * theme: 'light', + * sidebarCollapsed: false, + * immersiveModeEnabled: false, + * }; + * + * export const settingsStore = createAppSettingsStore('my-app-settings', DEFAULT_SETTINGS); + * + * // Usage: + * settingsStore.settings.theme // 'light' + * settingsStore.set('theme', 'dark'); + * settingsStore.update({ sidebarCollapsed: true }); + * settingsStore.toggleImmersiveMode(); // If immersiveModeEnabled exists + * ``` + */ + +import { browser } from '$app/environment'; + +export interface AppSettingsStoreOptions { + /** + * Callback invoked after each settings change. + * Use for cloud sync or other side effects. + */ + onSettingsChange?: (settings: T) => void | Promise; +} + +export interface AppSettingsStore> { + /** Current settings state (reactive) */ + readonly settings: T; + + /** Get the default settings */ + getDefaults(): T; + + /** Initialize settings from localStorage (call on mount) */ + initialize(): void; + + /** Set a single setting value */ + set(key: K, value: T[K]): void; + + /** Update multiple settings at once */ + update(updates: Partial): void; + + /** Reset all settings to defaults */ + reset(): void; + + /** Toggle immersive mode (if immersiveModeEnabled exists in settings) */ + toggleImmersiveMode(): void; +} + +/** + * Creates a settings store with localStorage persistence. + * + * @param storageKey - localStorage key for persistence + * @param defaultSettings - Default settings object + * @param options - Optional configuration (onSettingsChange callback) + * @returns AppSettingsStore instance + */ +export function createAppSettingsStore>( + storageKey: string, + defaultSettings: T, + options?: AppSettingsStoreOptions +): AppSettingsStore { + // Load settings from localStorage + function loadSettings(): T { + if (!browser) return { ...defaultSettings }; + + try { + const stored = localStorage.getItem(storageKey); + if (stored) { + const parsed = JSON.parse(stored); + // Merge with defaults to handle new settings added after initial save + return { ...defaultSettings, ...parsed }; + } + } catch (e) { + console.error(`Failed to load settings from ${storageKey}:`, e); + } + + return { ...defaultSettings }; + } + + // Save settings to localStorage + function saveSettings(newSettings: T): void { + if (!browser) return; + + try { + localStorage.setItem(storageKey, JSON.stringify(newSettings)); + } catch (e) { + console.error(`Failed to save settings to ${storageKey}:`, e); + } + + // Invoke callback if provided + options?.onSettingsChange?.(newSettings); + } + + // Reactive state using Svelte 5 runes + let settings = $state(loadSettings()); + + return { + get settings() { + return settings; + }, + + getDefaults() { + return { ...defaultSettings }; + }, + + initialize() { + if (!browser) return; + settings = loadSettings(); + }, + + set(key: K, value: T[K]) { + settings = { ...settings, [key]: value }; + saveSettings(settings); + }, + + update(updates: Partial) { + settings = { ...settings, ...updates }; + saveSettings(settings); + }, + + reset() { + settings = { ...defaultSettings }; + saveSettings(settings); + }, + + toggleImmersiveMode() { + if ('immersiveModeEnabled' in settings) { + const current = settings.immersiveModeEnabled as boolean; + settings = { ...settings, immersiveModeEnabled: !current }; + saveSettings(settings); + } + }, + }; +}