refactor(todo): unify task views into single page with collapsible sections

- Remove separate /today, /upcoming, and /completed routes
- Add unified view with CollapsibleSection components for Overdue, Today, Upcoming, and Completed
- Add new fetchAllTasks() method to tasks store
- Simplify navigation from 4 items to 2 (Tasks + Settings)
- Refactor settings page with shared-ui components and theme controls

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-07 16:06:08 +01:00
parent a6cc0b83aa
commit e3ba35b20e
8 changed files with 490 additions and 328 deletions

View file

@ -0,0 +1,133 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import {
Warning,
CalendarBlank,
CalendarDots,
CheckCircle,
CaretDown,
} from '@manacore/shared-icons';
interface Props {
title: string;
count: number;
icon?: 'warning' | 'today' | 'upcoming' | 'completed';
variant?: 'default' | 'warning' | 'success';
defaultOpen?: boolean;
children: Snippet;
}
let { title, count, icon, variant = 'default', defaultOpen = true, children }: Props = $props();
let isOpen = $state(defaultOpen);
// Icon colors based on variant
const iconColors = {
default: 'text-muted-foreground',
warning: 'text-red-500',
success: 'text-green-600 dark:text-green-500',
};
// Header text colors
const headerColors = {
default: 'text-foreground',
warning: 'text-red-600 dark:text-red-400',
success: 'text-green-700 dark:text-green-400',
};
</script>
<div class="section mb-3">
<button
type="button"
onclick={() => (isOpen = !isOpen)}
class="section-header glass-pill w-full flex items-center gap-3 px-4 py-3 rounded-full cursor-pointer transition-all duration-200"
>
<!-- Icon -->
<span class="icon-wrapper {iconColors[variant]}">
{#if icon === 'warning'}
<Warning size={20} weight="bold" />
{:else if icon === 'today'}
<CalendarBlank size={20} weight="bold" />
{:else if icon === 'upcoming'}
<CalendarDots size={20} weight="bold" />
{:else if icon === 'completed'}
<CheckCircle size={20} weight="bold" />
{/if}
</span>
<!-- Title -->
<span class="title font-semibold {headerColors[variant]}">{title}</span>
<!-- Count -->
<span class="count text-sm text-muted-foreground">({count})</span>
<!-- Chevron -->
<span
class="chevron ml-auto text-muted-foreground transition-transform duration-200"
class:rotate-180={isOpen}
>
<CaretDown size={18} weight="bold" />
</span>
</button>
{#if isOpen}
<div class="section-content mt-3 pl-1">
{@render children()}
</div>
{/if}
</div>
<style>
/* Glass pill effect matching PillNavigation */
.glass-pill {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
position: relative;
z-index: 1;
}
:global(.dark) .glass-pill {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.glass-pill:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .glass-pill:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.25);
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.section-content {
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -112,6 +112,33 @@ export const tasksStore = {
}
},
/**
* Fetch all tasks (incomplete + completed) for unified view
*/
async fetchAllTasks() {
loading = true;
error = null;
try {
// Fetch both incomplete and completed tasks
const [incompleteTasks, completedTasks] = await Promise.all([
tasksApi.getTasks({ isCompleted: false }),
tasksApi.getTasks({ isCompleted: true }),
]);
// Deduplicate tasks by ID (in case API returns duplicates)
const allTasks = [...incompleteTasks, ...completedTasks];
const uniqueTasksMap = new Map<string, Task>();
for (const task of allTasks) {
uniqueTasksMap.set(task.id, task);
}
tasks = Array.from(uniqueTasksMap.values());
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to fetch all tasks';
console.error('Failed to fetch all tasks:', e);
} finally {
loading = false;
}
},
/**
* Get tasks for a specific project
*/

View file

@ -65,10 +65,7 @@
// Navigation items for Todo
const navItems: PillNavItem[] = [
{ href: '/', label: 'Inbox', icon: 'inbox' },
{ href: '/today', label: 'Heute', icon: 'calendar-day' },
{ href: '/upcoming', label: 'Demnächst', icon: 'calendar' },
{ href: '/completed', label: 'Erledigt', icon: 'check-circle' },
{ href: '/', label: 'Aufgaben', icon: 'list' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
];

View file

@ -1,38 +1,86 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { format, addDays, startOfDay } from 'date-fns';
import { de } from 'date-fns/locale';
import { ListChecks } from '@manacore/shared-icons';
import { authStore } from '$lib/stores/auth.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { viewStore } from '$lib/stores/view.svelte';
import TaskList from '$lib/components/TaskList.svelte';
import QuickAddTask from '$lib/components/QuickAddTask.svelte';
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte';
import type { Task } from '@todo/shared';
let isLoading = $state(true);
onMount(async () => {
// Redirect to login if not authenticated
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Set view to inbox
viewStore.setInbox();
// Fetch inbox tasks
await tasksStore.fetchInboxTasks();
viewStore.setToday();
await tasksStore.fetchAllTasks();
isLoading = false;
});
// Derived task lists
let overdueTasks = $derived(tasksStore.overdueTasks);
let todayTasks = $derived(tasksStore.todayTasks);
let completedTasks = $derived(tasksStore.completedTasks);
// Group upcoming tasks by day
let groupedUpcomingTasks = $derived(() => {
const groups: { date: Date; label: string; tasks: Task[] }[] = [];
const today = startOfDay(new Date());
// Start from tomorrow (day 1) through day 7
for (let i = 1; i <= 7; i++) {
const date = addDays(today, i);
const dayTasks = tasksStore.tasks.filter((task) => {
if (!task.dueDate || task.isCompleted) return false;
const taskDate = startOfDay(new Date(task.dueDate));
return taskDate.getTime() === date.getTime();
});
if (dayTasks.length > 0) {
let label: string;
if (i === 1) {
label = 'Morgen';
} else {
label = format(date, 'EEEE, d. MMMM', { locale: de });
}
groups.push({ date, label, tasks: dayTasks });
}
}
return groups;
});
// Total upcoming count
let upcomingCount = $derived(
groupedUpcomingTasks().reduce((sum, group) => sum + group.tasks.length, 0)
);
// Check if all sections are empty
let allEmpty = $derived(
overdueTasks.length === 0 &&
todayTasks.length === 0 &&
upcomingCount === 0 &&
completedTasks.length === 0
);
</script>
<svelte:head>
<title>Inbox | Todo</title>
<title>Todo</title>
</svelte:head>
<div class="inbox-view">
<div class="unified-view">
<header class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Inbox</h1>
<p class="text-muted-foreground text-sm mt-1">Aufgaben ohne Projekt</p>
<h1 class="text-2xl font-bold text-foreground">Meine Aufgaben</h1>
<p class="text-muted-foreground text-sm mt-1">Alle deine Aufgaben auf einen Blick</p>
</header>
<QuickAddTask />
@ -47,28 +95,94 @@
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
{tasksStore.error}
</div>
{:else if tasksStore.incompleteTasks.length === 0}
{:else if allEmpty}
<div class="text-center py-12">
<div class="text-6xl mb-4">📥</div>
<h3 class="text-lg font-medium text-foreground mb-2">Inbox ist leer</h3>
<p class="text-muted-foreground">Füge eine neue Aufgabe hinzu, um loszulegen.</p>
<div class="flex justify-center mb-4 text-muted-foreground">
<ListChecks size={48} weight="light" />
</div>
<h3 class="text-lg font-medium text-foreground mb-2">Noch keine Aufgaben</h3>
<p class="text-muted-foreground">Erstelle deine erste Aufgabe mit dem Eingabefeld oben.</p>
</div>
{:else}
<TaskList tasks={tasksStore.incompleteTasks} />
{/if}
<div class="space-y-2">
<!-- Overdue Section - only show if there are overdue tasks -->
{#if overdueTasks.length > 0}
<CollapsibleSection
title="Überfällig"
count={overdueTasks.length}
icon="warning"
variant="warning"
defaultOpen={true}
>
<TaskList tasks={overdueTasks} />
</CollapsibleSection>
{/if}
{#if viewStore.showCompleted && tasksStore.completedTasks.length > 0}
<div class="mt-8">
<h2 class="text-lg font-semibold text-muted-foreground mb-4">
Erledigt ({tasksStore.completedTasks.length})
</h2>
<TaskList tasks={tasksStore.completedTasks} showCompleted />
<!-- Today Section -->
<CollapsibleSection
title="Heute"
count={todayTasks.length}
icon="today"
variant="default"
defaultOpen={true}
>
{#if todayTasks.length === 0}
<div class="text-center py-6 text-muted-foreground">
<p>Keine Aufgaben für heute</p>
</div>
{:else}
<TaskList tasks={todayTasks} />
{/if}
</CollapsibleSection>
<!-- Upcoming Section -->
<CollapsibleSection
title="Demnächst"
count={upcomingCount}
icon="upcoming"
variant="default"
defaultOpen={true}
>
{#if upcomingCount === 0}
<div class="text-center py-6 text-muted-foreground">
<p>Keine anstehenden Aufgaben</p>
</div>
{:else}
<div class="space-y-4">
{#each groupedUpcomingTasks() as group}
<div>
<h3 class="text-sm font-medium text-muted-foreground mb-2 pl-2">
{group.label} ({group.tasks.length})
</h3>
<TaskList tasks={group.tasks} />
</div>
{/each}
</div>
{/if}
</CollapsibleSection>
<!-- Completed Section - collapsed by default -->
<CollapsibleSection
title="Erledigt"
count={completedTasks.length}
icon="completed"
variant="success"
defaultOpen={false}
>
{#if completedTasks.length === 0}
<div class="text-center py-6 text-muted-foreground">
<p>Noch keine erledigten Aufgaben</p>
</div>
{:else}
<TaskList tasks={completedTasks} showCompleted />
{/if}
</CollapsibleSection>
</div>
{/if}
</div>
<style>
.inbox-view {
.unified-view {
padding-bottom: 100px;
}
</style>

View file

@ -1,58 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { viewStore } from '$lib/stores/view.svelte';
import TaskList from '$lib/components/TaskList.svelte';
let isLoading = $state(true);
onMount(async () => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
viewStore.setCompleted();
await tasksStore.fetchTasks({ isCompleted: true });
isLoading = false;
});
</script>
<svelte:head>
<title>Erledigt | Todo</title>
</svelte:head>
<div class="completed-view">
<header class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Erledigt</h1>
<p class="text-muted-foreground text-sm mt-1">Erledigte Aufgaben</p>
</header>
{#if isLoading || tasksStore.loading}
<div class="flex items-center justify-center py-12">
<div
class="animate-spin h-8 w-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if tasksStore.error}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
{tasksStore.error}
</div>
{:else if tasksStore.completedTasks.length === 0}
<div class="text-center py-12">
<div class="text-6xl mb-4"></div>
<h3 class="text-lg font-medium text-foreground mb-2">Noch nichts erledigt</h3>
<p class="text-muted-foreground">Erledigte Aufgaben werden hier angezeigt.</p>
</div>
{:else}
<TaskList tasks={tasksStore.completedTasks} showCompleted />
{/if}
</div>
<style>
.completed-view {
padding-bottom: 100px;
}
</style>

View file

@ -2,75 +2,215 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
import { ThemeColorPreview } from '@manacore/shared-theme-ui';
import {
SettingsPage,
SettingsSection,
SettingsCard,
SettingsRow,
SettingsToggle,
SettingsDangerZone,
SettingsDangerButton,
GlobalSettingsSection,
} from '@manacore/shared-ui';
onMount(() => {
onMount(async () => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Load user settings from server
await userSettings.load();
});
function toggleDarkMode() {
theme.toggleMode();
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
</script>
<svelte:head>
<title>Einstellungen | Todo</title>
</svelte:head>
<div class="settings-view">
<header class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Einstellungen</h1>
</header>
<SettingsPage title="Einstellungen" subtitle="Verwalte dein Konto und passe die App an.">
<!-- Account Section -->
<SettingsSection title="Konto">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
{/snippet}
<div class="space-y-6">
<!-- Account Section -->
<section class="bg-card rounded-lg border border-border p-6">
<h2 class="text-lg font-semibold text-foreground mb-4">Konto</h2>
<SettingsCard>
<SettingsRow label="E-Mail" description={authStore.user?.email || 'Nicht angemeldet'}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
{/snippet}
</SettingsRow>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-foreground">E-Mail</p>
<p class="text-sm text-muted-foreground">
{authStore.user?.email || 'Nicht angemeldet'}
</p>
</div>
</div>
<SettingsRow label="Konto-Status" description="Dein aktueller Kontostatus" border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
{/snippet}
<span
class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-900/20 dark:text-green-400"
>
Aktiv
</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<div class="pt-4 border-t border-border">
<button
class="text-red-500 hover:text-red-600 text-sm font-medium"
onclick={() => {
authStore.signOut();
goto('/login');
}}
>
Abmelden
</button>
<!-- Appearance Section -->
<SettingsSection title="Aussehen">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
{/snippet}
<SettingsCard>
<SettingsToggle
label="Dark Mode"
description="Dunkles Farbschema verwenden"
isOn={theme.isDark}
onToggle={toggleDarkMode}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/snippet}
</SettingsToggle>
<SettingsRow
label="Aktuelles Theme"
description={THEME_DEFINITIONS[theme.variant].label}
onclick={() => goto('/themes')}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
{/snippet}
<span
class="px-3 py-1.5 text-sm font-medium bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] rounded-lg"
>
Themes wählen
</span>
</SettingsRow>
<div class="px-5 py-4 border-t border-[hsl(var(--border))]">
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Farbvorschau</p>
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">
So sieht die App mit dem aktuellen Theme aus
</p>
<div class="flex justify-center">
<ThemeColorPreview
variant={theme.variant}
mode={theme.isDark ? 'dark' : 'light'}
size="lg"
/>
</div>
</div>
</section>
</SettingsCard>
</SettingsSection>
<!-- Appearance Section -->
<section class="bg-card rounded-lg border border-border p-6">
<h2 class="text-lg font-semibold text-foreground mb-4">Erscheinungsbild</h2>
<!-- Global Settings Section -->
<GlobalSettingsSection {userSettings} appId="todo" />
<p class="text-sm text-muted-foreground">
Theme-Einstellungen sind in der Navigation verfügbar.
</p>
</section>
<!-- About Section -->
<SettingsSection title="Über">
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/snippet}
<!-- About Section -->
<section class="bg-card rounded-lg border border-border p-6">
<h2 class="text-lg font-semibold text-foreground mb-4">Über</h2>
<SettingsCard>
<SettingsRow label="Version" border={false}>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
/>
</svg>
{/snippet}
<span class="text-[hsl(var(--muted-foreground))]">1.0.0</span>
</SettingsRow>
</SettingsCard>
</SettingsSection>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">Todo App v1.0.0</p>
<p class="text-sm text-muted-foreground">Teil des ManaCore Ökosystems</p>
</div>
</section>
</div>
</div>
<style>
.settings-view {
padding-bottom: 100px;
}
</style>
<!-- Danger Zone -->
<SettingsDangerZone title="Gefahrenzone">
<SettingsDangerButton
label="Abmelden"
description="Von deinem Konto abmelden"
buttonText="Abmelden"
onclick={handleLogout}
border={false}
>
{#snippet icon()}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{/snippet}
</SettingsDangerButton>
</SettingsDangerZone>
</SettingsPage>

View file

@ -1,91 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { viewStore } from '$lib/stores/view.svelte';
import TaskList from '$lib/components/TaskList.svelte';
import QuickAddTask from '$lib/components/QuickAddTask.svelte';
let isLoading = $state(true);
onMount(async () => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
viewStore.setToday();
await tasksStore.fetchTodayTasks();
isLoading = false;
});
// Separate overdue and today tasks
let overdueTasks = $derived(tasksStore.overdueTasks);
let todayTasks = $derived(tasksStore.todayTasks);
</script>
<svelte:head>
<title>Heute | Todo</title>
</svelte:head>
<div class="today-view">
<header class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Heute</h1>
<p class="text-muted-foreground text-sm mt-1">Fällige Aufgaben für heute</p>
</header>
<QuickAddTask />
{#if isLoading || tasksStore.loading}
<div class="flex items-center justify-center py-12">
<div
class="animate-spin h-8 w-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if tasksStore.error}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
{tasksStore.error}
</div>
{:else}
{#if overdueTasks.length > 0}
<div class="mb-6">
<h2 class="text-sm font-semibold text-red-500 mb-3 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
Überfällig ({overdueTasks.length})
</h2>
<TaskList tasks={overdueTasks} />
</div>
{/if}
{#if todayTasks.length > 0}
<div>
<h2 class="text-sm font-semibold text-muted-foreground mb-3">
Heute ({todayTasks.length})
</h2>
<TaskList tasks={todayTasks} />
</div>
{/if}
{#if overdueTasks.length === 0 && todayTasks.length === 0}
<div class="text-center py-12">
<div class="text-6xl mb-4">🎉</div>
<h3 class="text-lg font-medium text-foreground mb-2">Alles erledigt!</h3>
<p class="text-muted-foreground">Keine Aufgaben für heute.</p>
</div>
{/if}
{/if}
</div>
<style>
.today-view {
padding-bottom: 100px;
}
</style>

View file

@ -1,100 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { format, addDays, startOfDay } from 'date-fns';
import { de } from 'date-fns/locale';
import { authStore } from '$lib/stores/auth.svelte';
import { tasksStore } from '$lib/stores/tasks.svelte';
import { viewStore } from '$lib/stores/view.svelte';
import TaskList from '$lib/components/TaskList.svelte';
import type { Task } from '@todo/shared';
let isLoading = $state(true);
onMount(async () => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
viewStore.setUpcoming();
await tasksStore.fetchUpcomingTasks();
isLoading = false;
});
// Group tasks by day
let groupedTasks = $derived(() => {
const groups: { date: Date; label: string; tasks: Task[] }[] = [];
const today = startOfDay(new Date());
for (let i = 0; i < 7; i++) {
const date = addDays(today, i);
const dayTasks = tasksStore.tasks.filter((task) => {
if (!task.dueDate || task.isCompleted) return false;
const taskDate = startOfDay(new Date(task.dueDate));
return taskDate.getTime() === date.getTime();
});
if (dayTasks.length > 0) {
let label: string;
if (i === 0) {
label = 'Heute';
} else if (i === 1) {
label = 'Morgen';
} else {
label = format(date, 'EEEE, d. MMMM', { locale: de });
}
groups.push({ date, label, tasks: dayTasks });
}
}
return groups;
});
</script>
<svelte:head>
<title>Demnächst | Todo</title>
</svelte:head>
<div class="upcoming-view">
<header class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Demnächst</h1>
<p class="text-muted-foreground text-sm mt-1">Aufgaben der nächsten 7 Tage</p>
</header>
{#if isLoading || tasksStore.loading}
<div class="flex items-center justify-center py-12">
<div
class="animate-spin h-8 w-8 border-4 border-primary border-r-transparent rounded-full"
></div>
</div>
{:else if tasksStore.error}
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
{tasksStore.error}
</div>
{:else if groupedTasks().length === 0}
<div class="text-center py-12">
<div class="text-6xl mb-4">📅</div>
<h3 class="text-lg font-medium text-foreground mb-2">Keine anstehenden Aufgaben</h3>
<p class="text-muted-foreground">Keine Aufgaben in den nächsten 7 Tagen geplant.</p>
</div>
{:else}
<div class="space-y-8">
{#each groupedTasks() as group}
<div>
<h2 class="text-sm font-semibold text-muted-foreground mb-3">
{group.label} ({group.tasks.length})
</h2>
<TaskList tasks={group.tasks} />
</div>
{/each}
</div>
{/if}
</div>
<style>
.upcoming-view {
padding-bottom: 100px;
}
</style>