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 ceaeab1a9..ae782cbdc 100644 --- a/apps/todo/apps/web/src/lib/stores/settings.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/settings.svelte.ts @@ -11,9 +11,23 @@ export type TodoView = 'inbox' | 'today' | 'upcoming' | 'kanban' | 'completed'; export type KanbanCardSize = 'compact' | 'normal' | 'large'; export type PageMode = 'date' | 'priority' | 'custom'; +export type PageIcon = + | 'warning' + | 'calendar' + | 'calendar-dots' + | 'check' + | 'star' + | 'lightning' + | 'clock' + | 'fire' + | 'leaf' + | 'heart'; +export type PageWidth = 'narrow' | 'medium' | 'wide' | 'full'; + export interface PageConfig { id: string; label: string; + icon?: PageIcon; filter: { priorities?: ('low' | 'medium' | 'high' | 'urgent')[]; completed?: boolean; @@ -64,6 +78,7 @@ export interface TodoAppSettings extends Record { // Page mode pageMode: PageMode; + pageWidth: PageWidth; customPages: PageConfig[]; } @@ -110,6 +125,7 @@ const DEFAULT_SETTINGS: TodoAppSettings = { // Page mode pageMode: 'priority' as PageMode, + pageWidth: 'medium' as PageWidth, customPages: [] as PageConfig[], }; @@ -205,6 +221,9 @@ export const todoSettings = { get pageMode() { return baseStore.settings.pageMode; }, + get pageWidth() { + return baseStore.settings.pageWidth; + }, get customPages() { return baseStore.settings.customPages; }, diff --git a/apps/todo/apps/web/src/routes/(app)/+page.svelte b/apps/todo/apps/web/src/routes/(app)/+page.svelte index ab6c2b0d5..e45940b25 100644 --- a/apps/todo/apps/web/src/routes/(app)/+page.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+page.svelte @@ -5,12 +5,32 @@ import { Sparkle, ArrowDown, + ArrowLeft, + ArrowRight, Warning, CalendarBlank, CalendarDots, CheckCircle, + Plus, + Trash, + PencilSimple, + GearSix, + X, + Star, + Lightning, + Clock, + Fire, + Leaf, + Heart, } from '@manacore/shared-icons'; import { tasksStore } from '$lib/stores/tasks.svelte'; + import { + todoSettings, + type PageConfig, + type PageMode, + type PageIcon, + type PageWidth, + } from '$lib/stores/settings.svelte'; import { viewStore } from '$lib/stores/view.svelte'; import { applyTaskFilters } from '$lib/utils/task-filters'; import { filterOverdue, filterToday, filterCompleted } from '$lib/data/task-queries'; @@ -18,18 +38,16 @@ import { TaskListSkeleton } from '$lib/components/skeletons'; import type { Task } from '@todo/shared'; - // Live tasks from layout context — auto-updates on IndexedDB changes const allTasks: { readonly value: Task[]; readonly loading: boolean; readonly error: unknown } = getContext('tasks'); let tipDismissed = $state(false); - let completedOpen = $state(false); + let editMode = $state(false); + let filterOpenId = $state(null); - // Stable date references (computed once, not on every re-render) const today = startOfDay(new Date()); const tomorrow = addDays(today, 1); - // Build filter criteria from viewStore (reactive) let filterCriteria = $derived({ priorities: viewStore.filterPriorities, projectId: viewStore.filterProjectId, @@ -45,12 +63,13 @@ viewStore.setToday(); }); - // Derived task lists (with filters applied) — automatically reactive via liveQuery - let overdueTasks = $derived(applyFilters(filterOverdue(allTasks.value))); - let todayTasks = $derived(applyFilters(filterToday(allTasks.value))); + // ── Derived task lists ── + let activeTasks = $derived(applyFilters(allTasks.value.filter((t) => !t.isCompleted))); let completedTasks = $derived(applyFilters(filterCompleted(allTasks.value))); - // Tomorrow's tasks + // Date-mode lists + let overdueTasks = $derived(applyFilters(filterOverdue(allTasks.value))); + let todayTasks = $derived(applyFilters(filterToday(allTasks.value))); let tomorrowTasks = $derived( applyFilters( allTasks.value.filter((task) => { @@ -60,11 +79,8 @@ }) ) ); - - // Group upcoming tasks by day (starting from day after tomorrow) let groupedUpcomingTasks = $derived.by(() => { const groups: { date: Date; label: string; tasks: Task[] }[] = []; - for (let i = 2; i <= 7; i++) { const date = addDays(today, i); const dayTasks = applyFilters( @@ -74,43 +90,61 @@ return taskDate.getTime() === date.getTime(); }) ); - if (dayTasks.length > 0) { - const label = format(date, 'EEEE, d. MMMM', { locale: de }); - groups.push({ date, label, tasks: dayTasks }); + groups.push({ + date, + label: format(date, 'EEEE, d. MMMM', { locale: de }), + tasks: dayTasks, + }); } } - return groups; }); + let upcomingCount = $derived(groupedUpcomingTasks.reduce((sum, g) => sum + g.tasks.length, 0)); - // Total upcoming count (excluding tomorrow) - let upcomingCount = $derived( - groupedUpcomingTasks.reduce((sum, group) => sum + group.tasks.length, 0) + // Priority-mode lists + let urgentImportant = $derived( + activeTasks.filter((t) => t.priority === 'urgent' || t.priority === 'high') + ); + let importantLater = $derived( + activeTasks.filter((t) => t.priority === 'medium' || t.priority === 'low' || !t.priority) ); - // Check if all sections are empty - let allEmpty = $derived( - overdueTasks.length === 0 && - todayTasks.length === 0 && - tomorrowTasks.length === 0 && - upcomingCount === 0 && - completedTasks.length === 0 + // Custom-mode filter + function filterByPageConfig(config: PageConfig): Task[] { + let tasks = config.filter.completed + ? applyFilters(allTasks.value.filter((t) => t.isCompleted)) + : activeTasks; + if (config.filter.priorities?.length) { + tasks = tasks.filter((t) => config.filter.priorities!.includes(t.priority as any)); + } + if (config.filter.dateRange && config.filter.dateRange !== 'any') { + tasks = tasks.filter((t) => { + if (!t.dueDate) return false; + const d = startOfDay(new Date(t.dueDate)); + switch (config.filter.dateRange) { + case 'overdue': + return d.getTime() < today.getTime(); + case 'today': + return d.getTime() === today.getTime(); + case 'tomorrow': + return d.getTime() === tomorrow.getTime(); + case 'upcoming': + return d.getTime() > tomorrow.getTime(); + default: + return true; + } + }); + } + return tasks; + } + + let customPageData = $derived( + todoSettings.customPages.map((c) => ({ config: c, tasks: filterByPageConfig(c) })) ); + let allEmpty = $derived(activeTasks.length === 0 && completedTasks.length === 0); + let showOnboardingTip = $derived(activeTasks.length > 0 && activeTasks.length <= 3); - // Section visibility logic - let showTodaySection = $derived(todayTasks.length > 0 || !allEmpty); - let showTomorrowSection = $derived(tomorrowTasks.length > 0); - let showUpcomingSection = $derived(upcomingCount > 0); - let showCompletedSection = $derived(completedTasks.length > 0); - - // Onboarding tip: show when user has 1-3 active tasks - let totalActiveTasks = $derived( - overdueTasks.length + todayTasks.length + tomorrowTasks.length + upcomingCount - ); - let showOnboardingTip = $derived(totalActiveTasks > 0 && totalActiveTasks <= 3); - - // Syntax example snippets for empty state const syntaxExamples = [ { text: 'Meeting morgen 14 Uhr', description: 'Datum & Uhrzeit' }, { text: 'Einkaufen #privat', description: 'Mit Label' }, @@ -121,19 +155,14 @@ window.dispatchEvent(new CustomEvent('quick-input-set', { detail: { text } })); } - // Drag and drop handler async function handleTaskDrop(taskId: string, targetDate: Date | 'completed' | 'overdue') { const task = allTasks.value.find((t) => t.id === taskId); if (!task) return; - if (targetDate === 'completed') { - if (!task.isCompleted) { - await tasksStore.updateTaskOptimistic(taskId, { isCompleted: true }); - } + if (!task.isCompleted) await tasksStore.updateTaskOptimistic(taskId, { isCompleted: true }); } else if (targetDate === 'overdue') { - const yesterday = subDays(today, 1); await tasksStore.updateTaskOptimistic(taskId, { - dueDate: yesterday.toISOString(), + dueDate: subDays(today, 1).toISOString(), isCompleted: task.isCompleted ? false : undefined, }); } else { @@ -144,40 +173,22 @@ } } - // Build pages array from visible sections - let pages = $derived.by(() => { - const p: { id: string; label: string; icon: string }[] = []; - if (overdueTasks.length > 0) p.push({ id: 'overdue', label: 'Überfällig', icon: 'warning' }); - if (showTodaySection) p.push({ id: 'today', label: 'Heute', icon: 'calendar' }); - if (showTomorrowSection) p.push({ id: 'tomorrow', label: 'Morgen', icon: 'calendar-dots' }); - if (showUpcomingSection) p.push({ id: 'upcoming', label: 'Demnächst', icon: 'calendar-dots' }); - if (showCompletedSection) p.push({ id: 'completed', label: 'Erledigt', icon: 'check' }); - return p; - }); - - let activePage = $state(0); + // Scroll tracking let scrollContainer: HTMLDivElement | undefined = $state(); - - function scrollToPage(index: number) { - if (!scrollContainer) return; - const sheets = scrollContainer.querySelectorAll('.notepad-sheet'); - if (sheets[index]) { - sheets[index].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); - } - } + let activePage = $state(0); + let sheetCount = $state(0); function handleScroll() { if (!scrollContainer) return; const sheets = scrollContainer.querySelectorAll('.notepad-sheet'); + sheetCount = sheets.length; const containerRect = scrollContainer.getBoundingClientRect(); const center = containerRect.left + containerRect.width / 2; - let closest = 0; let closestDist = Infinity; sheets.forEach((sheet, i) => { const rect = sheet.getBoundingClientRect(); - const sheetCenter = rect.left + rect.width / 2; - const dist = Math.abs(sheetCenter - center); + const dist = Math.abs(rect.left + rect.width / 2 - center); if (dist < closestDist) { closestDist = dist; closest = i; @@ -185,6 +196,183 @@ }); activePage = closest; } + + $effect(() => { + if (scrollContainer) sheetCount = scrollContainer.querySelectorAll('.notepad-sheet').length; + }); + + // ── Inline edit mode helpers ── + const PAGE_WIDTH_MAP: Record = { + narrow: 'min(640px, 85vw)', + medium: 'min(840px, 85vw)', + wide: 'min(1024px, 92vw)', + full: '92vw', + }; + + let sheetWidth = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium); + + const ICON_OPTIONS: { value: PageIcon; label: string }[] = [ + { value: 'warning', label: '⚠️' }, + { value: 'calendar', label: '📅' }, + { value: 'calendar-dots', label: '📆' }, + { value: 'check', label: '✅' }, + { value: 'star', label: '⭐' }, + { value: 'lightning', label: '⚡' }, + { value: 'clock', label: '🕐' }, + { value: 'fire', label: '🔥' }, + { value: 'leaf', label: '🍃' }, + { value: 'heart', label: '❤️' }, + ]; + + function setPageIcon(id: string, icon: PageIcon) { + const pages = clonePages(todoSettings.customPages); + const p = pages.find((pg: PageConfig) => pg.id === id); + if (p) { + p.icon = icon; + todoSettings.set('customPages', pages); + } + } + + const WIDTH_OPTIONS: { value: PageWidth; label: string }[] = [ + { value: 'narrow', label: 'S' }, + { value: 'medium', label: 'M' }, + { value: 'wide', label: 'L' }, + { value: 'full', label: 'XL' }, + ]; + + const PRIORITY_CHIPS = [ + { value: 'urgent' as const, label: 'Dringend', color: '#ef4444' }, + { value: 'high' as const, label: 'Hoch', color: '#f97316' }, + { value: 'medium' as const, label: 'Mittel', color: '#eab308' }, + { value: 'low' as const, label: 'Niedrig', color: '#22c55e' }, + ]; + + function clonePages(pages: PageConfig[]): PageConfig[] { + return JSON.parse(JSON.stringify(pages)); + } + + function ensureCustomPages(): PageConfig[] { + if (todoSettings.customPages.length > 0) return clonePages(todoSettings.customPages); + if (todoSettings.pageMode === 'priority') { + return [ + { + id: crypto.randomUUID(), + label: 'Wichtig & Dringend', + icon: 'fire' as PageIcon, + filter: { priorities: ['urgent', 'high'] }, + }, + { + id: crypto.randomUUID(), + label: 'Wichtig & Später', + icon: 'calendar-dots' as PageIcon, + filter: { priorities: ['medium', 'low'] }, + }, + { + id: crypto.randomUUID(), + label: 'Erledigt', + icon: 'check' as PageIcon, + filter: { completed: true }, + }, + ]; + } + return [ + { + id: crypto.randomUUID(), + label: 'Heute', + icon: 'calendar' as PageIcon, + filter: { dateRange: 'today' }, + }, + { + id: crypto.randomUUID(), + label: 'Morgen', + icon: 'calendar-dots' as PageIcon, + filter: { dateRange: 'tomorrow' }, + }, + { + id: crypto.randomUUID(), + label: 'Erledigt', + icon: 'check' as PageIcon, + filter: { completed: true }, + }, + ]; + } + + function enterEditMode() { + const pages = ensureCustomPages(); + todoSettings.set('customPages', pages); + todoSettings.set('pageMode', 'custom'); + editMode = true; + } + + function exitEditMode() { + editMode = false; + filterOpenId = null; + } + + function updatePageLabel(id: string, label: string) { + const pages = clonePages(todoSettings.customPages); + const p = pages.find((pg: PageConfig) => pg.id === id); + if (p) { + p.label = label; + todoSettings.set('customPages', pages); + } + } + + function removePage(id: string) { + todoSettings.set( + 'customPages', + todoSettings.customPages.filter((p: PageConfig) => p.id !== id) + ); + if (filterOpenId === id) filterOpenId = null; + } + + function addPage() { + const newPage: PageConfig = { id: crypto.randomUUID(), label: 'Neue Seite', filter: {} }; + todoSettings.set('customPages', [...todoSettings.customPages, newPage]); + filterOpenId = newPage.id; + // Scroll to end after render + requestAnimationFrame(() => { + if (scrollContainer) + scrollContainer.scrollTo({ left: scrollContainer.scrollWidth, behavior: 'smooth' }); + }); + } + + function togglePriority(id: string, priority: 'low' | 'medium' | 'high' | 'urgent') { + const pages = clonePages(todoSettings.customPages); + const p = pages.find((pg: PageConfig) => pg.id === id); + if (!p) return; + const cur = p.filter.priorities || []; + p.filter.priorities = cur.includes(priority) + ? cur.filter((x: string) => x !== priority) + : [...cur, priority]; + if (p.filter.priorities.length === 0) p.filter.priorities = undefined; + todoSettings.set('customPages', pages); + } + + function setDateRange(id: string, val: string) { + const pages = clonePages(todoSettings.customPages); + const p = pages.find((pg: PageConfig) => pg.id === id); + if (!p) return; + p.filter.dateRange = val === 'any' ? undefined : (val as any); + todoSettings.set('customPages', pages); + } + + function toggleCompleted(id: string) { + const pages = clonePages(todoSettings.customPages); + const p = pages.find((pg: PageConfig) => pg.id === id); + if (!p) return; + p.filter.completed = p.filter.completed ? undefined : true; + todoSettings.set('customPages', pages); + } + + function movePage(id: string, dir: -1 | 1) { + const pages = clonePages(todoSettings.customPages); + const idx = pages.findIndex((p: PageConfig) => p.id === id); + const target = idx + dir; + if (target < 0 || target >= pages.length) return; + [pages[idx], pages[target]] = [pages[target], pages[idx]]; + todoSettings.set('customPages', pages); + } @@ -193,6 +381,10 @@ { + if (e.key === 'Escape' && editMode) { + exitEditMode(); + return; + } const target = e.target as HTMLElement; const isInQuickInput = target.closest('.quick-input-bar'); if (isInQuickInput && (e.key === 'ArrowUp' || (e.key === 'Tab' && !e.shiftKey))) { @@ -208,9 +400,7 @@ {#if allTasks.loading}
-
- -
+
{:else if allTasks.error} @@ -223,21 +413,17 @@ -{:else if allEmpty} +{:else if allEmpty && !editMode}
-
- -
+

Bereit für einen produktiven Tag

Tippe unten um loszulegen...

-
- -
+

Schnellstart-Tipps

@@ -247,10 +433,8 @@ type="button" class="example-chip" onclick={() => handleExampleClick(example.text)} - title={example.description} + title={example.description}>{example.text} - {example.text} - {/each}
@@ -260,47 +444,267 @@
{:else} - - {#if pages.length > 1} -
- {#each pages as page, i} - - {/each} -
- {/if} +
+ {#if editMode} +
+ Seitenbreite +
+ {#each WIDTH_OPTIONS as opt} + + {/each} +
+
+ {/if} +
+ {#if todoSettings.pageMode === 'custom' || editMode} + + {#each customPageData as { config, tasks }, pageIdx (config.id)} +
+ {#if editMode && pageIdx > 0} + + {/if} +
+ {#if editMode} +
+ + updatePageLabel(config.id, (e.target as HTMLInputElement).value)} + /> + + +
-
-
- - {#if overdueTasks.length > 0} + {#if filterOpenId === config.id} +
+
+ Icon +
+ {#each ICON_OPTIONS as opt} + + {/each} +
+
+
+ Prioritäten +
+ {#each PRIORITY_CHIPS as chip} + + {/each} +
+
+
+ Zeitraum + +
+
+ +
+
+ {/if} + {:else} +
p === 'urgent' || p === 'high' + )} + > + {#if config.icon === 'star'} + {:else if config.icon === 'lightning'} + {:else if config.icon === 'clock'} + {:else if config.icon === 'fire'} + {:else if config.icon === 'leaf'} + {:else if config.icon === 'heart'} + {:else if config.icon === 'check' || config.filter.completed} + {:else if config.icon === 'warning'} + {:else if config.icon === 'calendar'} + {:else} + {/if} + {config.label} + {#if tasks.length > 0}{tasks.length}{/if} +
+ {/if} + +
+ +
+
+ {#if editMode && pageIdx < customPageData.length - 1} + + {/if} +
+ {/each} + + {#if editMode} +
e.key === 'Enter' && addPage()} + > + + Neue Seite +
+ {/if} + {:else if todoSettings.pageMode === 'priority'} + + {#if urgentImportant.length > 0} +
+
+ Wichtig & Dringend{urgentImportant.length} +
+
+ +
+
+ {/if}
-
- - Überfällig - {overdueTasks.length} +
+ Wichtig & Später{importantLater.length}
+ {#if showOnboardingTip && !tipDismissed} +
+ 💡 + Tipp: Nutze !hoch oder !dringend um Tasks auf die erste + Seite zu setzen + +
+ {/if}
- {/if} - - - {#if showTodaySection} + {#if completedTasks.length > 0} +
+
+ Erledigt{completedTasks.length} +
+
+ +
+
+ {/if} + {:else} + + {#if overdueTasks.length > 0} +
+
+ Überfällig{overdueTasks.length} +
+
+ +
+
+ {/if}
- - Heute - {#if todayTasks.length > 0} - {todayTasks.length} - {/if} + Heute{#if todayTasks.length > 0}{todayTasks.length}{/if}
- {#if showOnboardingTip && !tipDismissed} -
- 💡 - - Tipp: Nutze #tags und !priorität für bessere Organisation - - -
- {/if}
- {/if} - - - {#if showTomorrowSection} -
-
- - Morgen - {tomorrowTasks.length} + {#if tomorrowTasks.length > 0} +
+
+ Morgen{tomorrowTasks.length} +
+
+ +
-
- + {/if} + {#if upcomingCount > 0} +
+
+ Demnächst{upcomingCount} +
+
+ {#each groupedUpcomingTasks as group} +
+

{group.label}

+ +
+ {/each} +
-
- {/if} - - - {#if showUpcomingSection} -
-
- - Demnächst - {upcomingCount} + {/if} + {#if completedTasks.length > 0} +
+
+ Erledigt{completedTasks.length} +
+
+ +
-
- {#each groupedUpcomingTasks as group} -
-

{group.label}

- -
- {/each} -
-
- {/if} - - - {#if showCompletedSection} -
-
- - Erledigt - {completedTasks.length} -
-
- -
-
+ {/if} {/if}
+ + + {#if sheetCount > 1 && !editMode} +
+ {#each Array(sheetCount) as _, i} +
+ {/each} +
+ {/if}
+ + + {/if}