From a5f5c8b63f0af2c4ee9897b4f2d95ae24d13dcc2 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 5 Apr 2026 17:55:12 +0200 Subject: [PATCH] feat(timeblocks): cross-module drag & drop + activity feed widget Cross-Module Drag & Drop: - WeekView day columns accept 'task' and 'habit' drops - Dropping a task auto-schedules it on that day (creates TimeBlock) - Dropping a habit creates a logged block on that day - HabitTile now has dragSource (long-press to drag) Activity Feed: - ActivityFeedWidget shows the 10 most recently updated timeBlocks - Shows type icon, title, action label (running/completed/planned), time ago - Registered as 'activity-feed' dashboard widget - i18n keys added (de + en) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/dashboard/widget-registry.ts | 2 + .../widgets/ActivityFeedWidget.svelte | 120 ++++++++++++++++++ .../src/lib/i18n/locales/dashboard/de.json | 5 + .../src/lib/i18n/locales/dashboard/en.json | 5 + .../calendar/components/WeekView.svelte | 39 ++++++ .../habits/components/HabitTile.svelte | 15 ++- .../apps/web/src/lib/types/dashboard.ts | 11 +- 7 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widget-registry.ts b/apps/manacore/apps/web/src/lib/components/dashboard/widget-registry.ts index 98dc2c7d0..f74846f7e 100644 --- a/apps/manacore/apps/web/src/lib/components/dashboard/widget-registry.ts +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widget-registry.ts @@ -31,6 +31,7 @@ import ActiveTimerWidget from '$lib/modules/core/widgets/ActiveTimerWidget.svelt import NutritionProgressWidget from '$lib/modules/core/widgets/NutritionProgressWidget.svelte'; import PlantWateringWidget from '$lib/modules/core/widgets/PlantWateringWidget.svelte'; import DayTimelineWidget from './widgets/DayTimelineWidget.svelte'; +import ActivityFeedWidget from './widgets/ActivityFeedWidget.svelte'; export const widgetComponents: Record = { credits: CreditsWidget, @@ -54,4 +55,5 @@ export const widgetComponents: Record = { 'nutrition-progress': NutritionProgressWidget, 'plant-watering': PlantWateringWidget, 'day-timeline': DayTimelineWidget, + 'activity-feed': ActivityFeedWidget, }; diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte new file mode 100644 index 000000000..0a749f0c4 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte @@ -0,0 +1,120 @@ + + +
+
+

+ + {$_('dashboard.widgets.activity_feed.title', { default: 'Aktivität' })} +

+
+ + {#if recentQuery.loading} +
+ {#each Array(4) as _} +
+ {/each} +
+ {:else if items.length === 0} +
+ +

+ {$_('dashboard.widgets.activity_feed.empty', { default: 'Noch keine Aktivität' })} +

+
+ {:else} +
+ {#each items as block (block.id)} + {@const TypeIcon = typeIcons[block.type] ?? CalendarBlank} + {@const habitIcon = + block.type === 'habit' && block.icon ? getIconComponent(block.icon) : null} +
+
+ {#if habitIcon} + + {:else} + + {/if} +
+ +
+ {block.title} +
+ +
+ + {actionLabel(block)} + + + {timeAgo(block.updatedAt)} + +
+
+ {/each} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/de.json b/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/de.json index b6765801d..a96a0d7f4 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/de.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/de.json @@ -139,6 +139,11 @@ "title": "Mein Tag", "description": "Chronologische Tagesansicht aller Aktivitäten", "empty": "Noch nichts heute" + }, + "activity_feed": { + "title": "Aktivität", + "description": "Letzte Änderungen über alle Module", + "empty": "Noch keine Aktivität" } } } diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/en.json b/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/en.json index 35745db7c..e1ead197e 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/en.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/dashboard/en.json @@ -139,6 +139,11 @@ "title": "My Day", "description": "Chronological timeline of all activities", "empty": "Nothing yet today" + }, + "activity_feed": { + "title": "Activity", + "description": "Recent changes across all modules", + "empty": "No activity yet" } } } diff --git a/apps/manacore/apps/web/src/lib/modules/calendar/components/WeekView.svelte b/apps/manacore/apps/web/src/lib/modules/calendar/components/WeekView.svelte index fbc98147e..09d731d39 100644 --- a/apps/manacore/apps/web/src/lib/modules/calendar/components/WeekView.svelte +++ b/apps/manacore/apps/web/src/lib/modules/calendar/components/WeekView.svelte @@ -2,6 +2,9 @@ import { onMount, getContext } from 'svelte'; import { calendarViewStore } from '../stores/view.svelte'; import { eventsStore } from '../stores/events.svelte'; + import { createBlock } from '$lib/data/time-blocks/service'; + import { dropTarget } from '@manacore/shared-ui/dnd'; + import type { DragPayload } from '@manacore/shared-ui/dnd'; import { getEventsForDay, getEventsInRange, @@ -172,6 +175,38 @@ onEventClick?.(event); } + /** Handle cross-module drop (task/habit onto calendar). */ + async function handleCrossModuleDrop(day: Date, payload: DragPayload) { + const data = payload.data as Record; + const defaultStart = new Date(day); + defaultStart.setHours(9, 0, 0, 0); + const defaultEnd = new Date(day); + defaultEnd.setHours(10, 0, 0, 0); + + if (payload.type === 'task') { + // Schedule task on calendar + const { tasksStore } = await import('$lib/modules/todo/stores/tasks.svelte'); + const dateStr = format(day, 'yyyy-MM-dd'); + await tasksStore.updateTask(data.id as string, { + _scheduleStartDate: dateStr, + _scheduleStartTime: '09:00', + }); + } else if (payload.type === 'habit') { + // Create a logged habit block at this day + await createBlock({ + startDate: defaultStart.toISOString(), + endDate: defaultEnd.toISOString(), + kind: 'logged', + type: 'habit', + sourceModule: 'habits', + sourceId: (data.id as string) || crypto.randomUUID(), + title: (data.title as string) || 'Habit', + color: (data.color as string) || null, + icon: (data.icon as string) || null, + }); + } + } + function formatHour(hour: number): string { return `${hour.toString().padStart(2, '0')}:00`; } @@ -245,6 +280,10 @@ dragToCreate.createTargetDay && isSameDay(day, dragToCreate.createTargetDay)} onpointerdown={dragToCreate.startCreate} + use:dropTarget={{ + accepts: ['task', 'habit'], + onDrop: (p) => handleCrossModuleDrop(day, p), + }} > {#each hours as hour}
diff --git a/apps/manacore/apps/web/src/lib/modules/habits/components/HabitTile.svelte b/apps/manacore/apps/web/src/lib/modules/habits/components/HabitTile.svelte index 6b2fbf69e..931080106 100644 --- a/apps/manacore/apps/web/src/lib/modules/habits/components/HabitTile.svelte +++ b/apps/manacore/apps/web/src/lib/modules/habits/components/HabitTile.svelte @@ -8,6 +8,7 @@ import { habitsStore } from '../stores/habits.svelte'; import { DynamicIcon } from '@manacore/shared-ui/atoms'; import { CaretRight } from '@manacore/shared-icons'; + import { dragSource } from '@manacore/shared-ui/dnd'; let { habit, @@ -62,7 +63,19 @@ ); -
+
({ + id: habit.id, + title: habit.title, + color: habit.color, + icon: habit.icon, + }), + longPressMs: 600, + }} +>