mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
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:
parent
a6cc0b83aa
commit
e3ba35b20e
8 changed files with 490 additions and 328 deletions
133
apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte
Normal file
133
apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte
Normal 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>
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue