From 863dd621f59bb176e79143d60bcf3ea628d6c42a Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:30:02 +0100 Subject: [PATCH] feat(todo): add comprehensive settings page with 20+ preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add global settings: confirmOnDelete, keyboardShortcutsEnabled - Create Todo-specific settings store with localStorage persistence - Add new shared-ui components: SettingsSelect, SettingsNumberInput, SettingsTimeInput - Redesign settings page with 6 new sections: - Task behavior (priority, due time, auto-archive, quick-add project) - View & display (default view, compact mode, task counts, subtask progress) - Kanban board (card size, labels, WIP limit) - Notifications (reminders, daily digest, overdue alerts) - Productivity (focus mode, pomodoro, daily goal, streak) - Keyboard shortcuts đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../web/src/lib/stores/settings.svelte.ts | 239 ++++++++ .../src/routes/(app)/settings/+page.svelte | 561 +++++++++++++++++- packages/shared-theme/src/types.ts | 6 + packages/shared-ui/src/index.ts | 3 + .../src/settings/SettingsNumberInput.svelte | 241 ++++++++ .../src/settings/SettingsSelect.svelte | 232 ++++++++ .../src/settings/SettingsTimeInput.svelte | 287 +++++++++ packages/shared-ui/src/settings/index.ts | 3 + 8 files changed, 1570 insertions(+), 2 deletions(-) create mode 100644 apps/todo/apps/web/src/lib/stores/settings.svelte.ts create mode 100644 packages/shared-ui/src/settings/SettingsNumberInput.svelte create mode 100644 packages/shared-ui/src/settings/SettingsSelect.svelte create mode 100644 packages/shared-ui/src/settings/SettingsTimeInput.svelte diff --git a/apps/todo/apps/web/src/lib/stores/settings.svelte.ts b/apps/todo/apps/web/src/lib/stores/settings.svelte.ts new file mode 100644 index 000000000..d2fe38e1e --- /dev/null +++ b/apps/todo/apps/web/src/lib/stores/settings.svelte.ts @@ -0,0 +1,239 @@ +/** + * Settings Store - Manages user preferences for the Todo app + * Uses Svelte 5 runes and localStorage for persistence + */ + +import { browser } from '$app/environment'; +import type { TaskPriority } from '@todo/shared'; + +// Settings types +export type TodoView = 'inbox' | 'today' | 'upcoming' | 'kanban' | 'completed'; +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; +} + +const DEFAULT_SETTINGS: TodoAppSettings = { + // Task Behavior + defaultPriority: 'medium', + defaultDueTime: '09:00', + autoArchiveCompletedDays: null, + quickAddProject: null, + + // View & Display + defaultView: 'inbox', + showTaskCounts: true, + compactMode: false, + showSubtaskProgress: true, + groupByProject: false, + + // Kanban Board + kanbanCardSize: 'normal', + showLabelsOnCards: true, + wipLimitPerColumn: null, + + // Notifications & Reminders + defaultReminderMinutes: null, + dailyDigestEnabled: false, + overdueNotifications: true, + + // Productivity + focusMode: false, + pomodoroEnabled: false, + dailyGoal: null, + showStreak: 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()); + +export const todoSettings = { + // Full settings object + get settings() { + return settings; + }, + + // Task Behavior + get defaultPriority() { + return settings.defaultPriority; + }, + get defaultDueTime() { + return settings.defaultDueTime; + }, + get autoArchiveCompletedDays() { + return settings.autoArchiveCompletedDays; + }, + get quickAddProject() { + return settings.quickAddProject; + }, + + // View & Display + get defaultView() { + return settings.defaultView; + }, + get showTaskCounts() { + return settings.showTaskCounts; + }, + get compactMode() { + return settings.compactMode; + }, + get showSubtaskProgress() { + return settings.showSubtaskProgress; + }, + get groupByProject() { + return settings.groupByProject; + }, + + // Kanban Board + get kanbanCardSize() { + return settings.kanbanCardSize; + }, + get showLabelsOnCards() { + return settings.showLabelsOnCards; + }, + get wipLimitPerColumn() { + return settings.wipLimitPerColumn; + }, + + // Notifications & Reminders + get defaultReminderMinutes() { + return settings.defaultReminderMinutes; + }, + get dailyDigestEnabled() { + return settings.dailyDigestEnabled; + }, + get overdueNotifications() { + return settings.overdueNotifications; + }, + + // Productivity + get focusMode() { + return settings.focusMode; + }, + get pomodoroEnabled() { + return settings.pomodoroEnabled; + }, + get dailyGoal() { + return settings.dailyGoal; + }, + get showStreak() { + return settings.showStreak; + }, + + /** + * 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; + }, +}; diff --git a/apps/todo/apps/web/src/routes/(app)/settings/+page.svelte b/apps/todo/apps/web/src/routes/(app)/settings/+page.svelte index 0ac1976c2..44600448a 100644 --- a/apps/todo/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/todo/apps/web/src/routes/(app)/settings/+page.svelte @@ -3,24 +3,71 @@ import { goto } from '$app/navigation'; import { authStore } from '$lib/stores/auth.svelte'; import { userSettings } from '$lib/stores/user-settings.svelte'; + import { todoSettings, type TodoView, type KanbanCardSize } from '$lib/stores/settings.svelte'; + import { projectsStore } from '$lib/stores/projects.svelte'; + import type { TaskPriority } from '@todo/shared'; import { SettingsPage, SettingsSection, SettingsCard, SettingsRow, + SettingsToggle, + SettingsSelect, + SettingsNumberInput, + SettingsTimeInput, SettingsDangerZone, SettingsDangerButton, GlobalSettingsSection, } from '@manacore/shared-ui'; + // Options for selects + const priorityOptions = [ + { value: 'low', label: 'Niedrig' }, + { value: 'medium', label: 'Mittel' }, + { value: 'high', label: 'Hoch' }, + { value: 'urgent', label: 'Dringend' }, + ]; + + const viewOptions = [ + { value: 'inbox', label: 'Inbox' }, + { value: 'today', label: 'Heute' }, + { value: 'upcoming', label: 'Anstehend' }, + { value: 'kanban', label: 'Kanban' }, + { value: 'completed', label: 'Erledigt' }, + ]; + + const cardSizeOptions = [ + { value: 'compact', label: 'Kompakt' }, + { value: 'normal', label: 'Normal' }, + { value: 'large', label: 'Groß' }, + ]; + + const reminderOptions = [ + { value: null, label: 'Keine' }, + { value: 5, label: '5 Minuten' }, + { value: 15, label: '15 Minuten' }, + { value: 30, label: '30 Minuten' }, + { value: 60, label: '1 Stunde' }, + { value: 1440, label: '1 Tag' }, + ]; + + // Project options for quick add (computed) + let projectOptions = $derived([ + { value: null, label: 'Inbox' }, + ...projectsStore.projects.map((p) => ({ value: p.id, label: p.name })), + ]); + onMount(async () => { if (!authStore.isAuthenticated) { goto('/login'); return; } - // Load user settings from server - await userSettings.load(); + // Load user settings and projects from server + await Promise.all([userSettings.load(), projectsStore.fetchProjects()]); + + // Initialize todo settings from localStorage + todoSettings.initialize(); }); async function handleLogout() { @@ -84,6 +131,516 @@ + + + {#snippet icon()} + + + + {/snippet} + + + + todoSettings.set('defaultPriority', v as TaskPriority)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('defaultDueTime', v)} + > + {#snippet icon()} + + + + {/snippet} + + + + todoSettings.set('quickAddProject', v as string | null)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('autoArchiveCompletedDays', v)} + min={1} + max={365} + placeholder="Aus" + > + {#snippet icon()} + + + + {/snippet} + + + userSettings.updateGeneral({ confirmOnDelete: v })} + border={false} + > + {#snippet icon()} + + + + {/snippet} + + + + + + + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('defaultView', v as TodoView)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('showTaskCounts', v)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('compactMode', v)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('showSubtaskProgress', v)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('groupByProject', v)} + border={false} + > + {#snippet icon()} + + + + {/snippet} + + + + + + + {#snippet icon()} + + + + {/snippet} + + + + todoSettings.set('kanbanCardSize', v as KanbanCardSize)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('showLabelsOnCards', v)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('wipLimitPerColumn', v)} + min={1} + max={50} + placeholder="Unbegrenzt" + border={false} + > + {#snippet icon()} + + + + {/snippet} + + + + + + + {#snippet icon()} + + + + {/snippet} + + + + todoSettings.set('defaultReminderMinutes', v as number | null)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('dailyDigestEnabled', v)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('overdueNotifications', v)} + border={false} + > + {#snippet icon()} + + + + {/snippet} + + + + + + + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('focusMode', v)} + > + {#snippet icon()} + + + + + {/snippet} + + + todoSettings.set('pomodoroEnabled', v)} + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('dailyGoal', v)} + min={1} + max={50} + placeholder="Kein Ziel" + > + {#snippet icon()} + + + + {/snippet} + + + todoSettings.set('showStreak', v)} + border={false} + > + {#snippet icon()} + + + + + {/snippet} + + + + + + + {#snippet icon()} + + + + {/snippet} + + + userSettings.updateGeneral({ keyboardShortcutsEnabled: v })} + border={false} + > + {#snippet icon()} + + + + {/snippet} + + + + {#snippet icon()} diff --git a/packages/shared-theme/src/types.ts b/packages/shared-theme/src/types.ts index bbad90c10..a65f2d43f 100644 --- a/packages/shared-theme/src/types.ts +++ b/packages/shared-theme/src/types.ts @@ -244,6 +244,10 @@ export interface GeneralSettings { weekStartsOn: WeekStartDay; /** Master toggle for all app sounds */ soundsEnabled: boolean; + /** Show confirmation dialog before deleting items */ + confirmOnDelete: boolean; + /** Enable keyboard shortcuts globally */ + keyboardShortcutsEnabled: boolean; } /** @@ -290,6 +294,8 @@ export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = { startPages: {}, // Empty = use app defaults weekStartsOn: 'monday', soundsEnabled: true, + confirmOnDelete: true, + keyboardShortcutsEnabled: true, }; /** diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index 1bc4d8705..445de250d 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -55,6 +55,9 @@ export { SettingsCard, SettingsRow, SettingsToggle, + SettingsSelect, + SettingsNumberInput, + SettingsTimeInput, SettingsDangerZone, SettingsDangerButton, GlobalSettingsSection, diff --git a/packages/shared-ui/src/settings/SettingsNumberInput.svelte b/packages/shared-ui/src/settings/SettingsNumberInput.svelte new file mode 100644 index 000000000..2098a061b --- /dev/null +++ b/packages/shared-ui/src/settings/SettingsNumberInput.svelte @@ -0,0 +1,241 @@ + + +
+
+ {#if icon} + + {@render icon()} + + {/if} +
+ {label} + {#if description} + {description} + {/if} +
+
+ + +
+ + diff --git a/packages/shared-ui/src/settings/SettingsSelect.svelte b/packages/shared-ui/src/settings/SettingsSelect.svelte new file mode 100644 index 000000000..0c8749a4b --- /dev/null +++ b/packages/shared-ui/src/settings/SettingsSelect.svelte @@ -0,0 +1,232 @@ + + +
+
+ {#if icon} + + {@render icon()} + + {/if} +
+ {label} + {#if description} + {description} + {/if} +
+
+ + +
+ + diff --git a/packages/shared-ui/src/settings/SettingsTimeInput.svelte b/packages/shared-ui/src/settings/SettingsTimeInput.svelte new file mode 100644 index 000000000..74c4ef370 --- /dev/null +++ b/packages/shared-ui/src/settings/SettingsTimeInput.svelte @@ -0,0 +1,287 @@ + + +
+
+ {#if icon} + + {@render icon()} + + {/if} +
+ {label} + {#if description} + {description} + {/if} +
+
+ +
+ + {#if value} + + {/if} +
+
+ + diff --git a/packages/shared-ui/src/settings/index.ts b/packages/shared-ui/src/settings/index.ts index 1a6bb0e74..f7daa394c 100644 --- a/packages/shared-ui/src/settings/index.ts +++ b/packages/shared-ui/src/settings/index.ts @@ -4,6 +4,9 @@ export { default as SettingsSection } from './SettingsSection.svelte'; export { default as SettingsCard } from './SettingsCard.svelte'; export { default as SettingsRow } from './SettingsRow.svelte'; export { default as SettingsToggle } from './SettingsToggle.svelte'; +export { default as SettingsSelect } from './SettingsSelect.svelte'; +export { default as SettingsNumberInput } from './SettingsNumberInput.svelte'; +export { default as SettingsTimeInput } from './SettingsTimeInput.svelte'; export { default as SettingsDangerZone } from './SettingsDangerZone.svelte'; export { default as SettingsDangerButton } from './SettingsDangerButton.svelte'; export { default as GlobalSettingsSection } from './GlobalSettingsSection.svelte';