mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 02:21:10 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
105f99459e
commit
a5f5c8b63f
7 changed files with 195 additions and 2 deletions
|
|
@ -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<WidgetType, Component> = {
|
||||
credits: CreditsWidget,
|
||||
|
|
@ -54,4 +55,5 @@ export const widgetComponents: Record<WidgetType, Component> = {
|
|||
'nutrition-progress': NutritionProgressWidget,
|
||||
'plant-watering': PlantWateringWidget,
|
||||
'day-timeline': DayTimelineWidget,
|
||||
'activity-feed': ActivityFeedWidget,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* ActivityFeedWidget — Recent timeBlock activity across all modules.
|
||||
*
|
||||
* Shows a chronological feed of recently created/updated timeBlocks.
|
||||
* Reads directly from IndexedDB, auto-updates via liveQuery.
|
||||
*/
|
||||
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalTimeBlock, TimeBlockType } from '$lib/data/time-blocks/types';
|
||||
import { toTimeBlock } from '$lib/data/time-blocks/queries';
|
||||
import type { TimeBlock } from '$lib/data/time-blocks/types';
|
||||
import {
|
||||
CalendarBlank,
|
||||
CheckSquare,
|
||||
Timer,
|
||||
Heart,
|
||||
Lightning,
|
||||
Clock,
|
||||
Activity,
|
||||
} from '@manacore/shared-icons';
|
||||
import { getIconComponent } from '@manacore/shared-icons';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
const MAX_ITEMS = 10;
|
||||
|
||||
const recentQuery = useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalTimeBlock>('timeBlocks').toArray();
|
||||
return locals
|
||||
.filter((b) => !b.deletedAt)
|
||||
.map(toTimeBlock)
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
.slice(0, MAX_ITEMS);
|
||||
}, [] as TimeBlock[]);
|
||||
|
||||
let items = $derived(recentQuery.value ?? []);
|
||||
|
||||
const typeIcons: Record<string, typeof CalendarBlank> = {
|
||||
event: CalendarBlank,
|
||||
task: CheckSquare,
|
||||
timeEntry: Timer,
|
||||
habit: Heart,
|
||||
focus: Lightning,
|
||||
break: Clock,
|
||||
};
|
||||
|
||||
function timeAgo(iso: string): string {
|
||||
return formatDistanceToNow(new Date(iso), { addSuffix: true, locale: de });
|
||||
}
|
||||
|
||||
function actionLabel(block: TimeBlock): string {
|
||||
if (block.isLive) return 'Läuft';
|
||||
if (block.linkedBlockId) return 'Erledigt';
|
||||
if (block.kind === 'scheduled') return 'Geplant';
|
||||
return 'Erfasst';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<Activity size={20} />
|
||||
{$_('dashboard.widgets.activity_feed.title', { default: 'Aktivität' })}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{#if recentQuery.loading}
|
||||
<div class="space-y-2">
|
||||
{#each Array(4) as _}
|
||||
<div class="h-8 animate-pulse rounded bg-surface-hover"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if items.length === 0}
|
||||
<div class="py-6 text-center">
|
||||
<Activity size={32} class="mx-auto mb-2 text-muted-foreground" />
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{$_('dashboard.widgets.activity_feed.empty', { default: 'Noch keine Aktivität' })}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-1">
|
||||
{#each items as block (block.id)}
|
||||
{@const TypeIcon = typeIcons[block.type] ?? CalendarBlank}
|
||||
{@const habitIcon =
|
||||
block.type === 'habit' && block.icon ? getIconComponent(block.icon) : null}
|
||||
<div
|
||||
class="flex items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-content-center rounded-full"
|
||||
class:animate-pulse={block.isLive}
|
||||
style="background: {block.color || '#6b7280'}20; color: {block.color || '#6b7280'}"
|
||||
>
|
||||
{#if habitIcon}
|
||||
<svelte:component this={habitIcon} size={12} />
|
||||
{:else}
|
||||
<svelte:component this={TypeIcon} size={12} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="truncate text-sm">{block.title}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 flex-col items-end">
|
||||
<span class="text-[0.625rem] font-medium text-muted-foreground">
|
||||
{actionLabel(block)}
|
||||
</span>
|
||||
<span class="text-[0.5625rem] text-muted-foreground">
|
||||
{timeAgo(block.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
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}
|
||||
<div class="hour-slot" role="button" tabindex="-1"></div>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<div class="habit-tile-wrapper">
|
||||
<div
|
||||
class="habit-tile-wrapper"
|
||||
use:dragSource={{
|
||||
type: 'habit',
|
||||
data: () => ({
|
||||
id: habit.id,
|
||||
title: habit.title,
|
||||
color: habit.color,
|
||||
icon: habit.icon,
|
||||
}),
|
||||
longPressMs: 600,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="habit-tile"
|
||||
class:pressing
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ export type WidgetType =
|
|||
| 'active-timer' // Times: running timer
|
||||
| 'nutrition-progress' // NutriPhi: today's calorie progress
|
||||
| 'plant-watering' // Planta: plants due for watering
|
||||
| 'day-timeline'; // TimeBlocks: chronological day timeline
|
||||
| 'day-timeline' // TimeBlocks: chronological day timeline
|
||||
| 'activity-feed'; // TimeBlocks: recent activity across modules
|
||||
|
||||
/**
|
||||
* Widget size - maps to CSS Grid columns
|
||||
|
|
@ -322,6 +323,14 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [
|
|||
defaultSize: 'medium',
|
||||
allowMultiple: false,
|
||||
},
|
||||
{
|
||||
type: 'activity-feed',
|
||||
nameKey: 'dashboard.widgets.activity_feed.title',
|
||||
descriptionKey: 'dashboard.widgets.activity_feed.description',
|
||||
icon: '📊',
|
||||
defaultSize: 'medium',
|
||||
allowMultiple: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue