diff --git a/apps/calendar/apps/web/src/lib/composables/useDragToCreate.svelte.ts b/apps/calendar/apps/web/src/lib/composables/useDragToCreate.svelte.ts index 411feefa3..df55f23a4 100644 --- a/apps/calendar/apps/web/src/lib/composables/useDragToCreate.svelte.ts +++ b/apps/calendar/apps/web/src/lib/composables/useDragToCreate.svelte.ts @@ -3,10 +3,8 @@ * Handles click-and-drag on the calendar grid to create new events */ -import { - SNAP_INTERVAL_MINUTES, - DEFAULT_EVENT_DURATION_MINUTES, -} from '$lib/utils/calendarConstants'; +import { DEFAULT_EVENT_DURATION_MINUTES } from '$lib/utils/calendarConstants'; +import { formatTime, getSnapMinutes, getDayFromX, getMinutesFromY } from '$lib/utils/drag-helpers'; export interface DragToCreateConfig { containerEl: HTMLElement | null; @@ -30,38 +28,21 @@ export function useDragToCreate(getConfig: () => DragToCreateConfig) { let createPreviewHeight = $state(0); let hasMoved = $state(false); - function getSnapMinutes(): number { - return getConfig().snapMinutes ?? SNAP_INTERVAL_MINUTES; + function dayFromX(clientX: number): Date | null { + const config = getConfig(); + return getDayFromX(clientX, config.containerEl, config.days); } - function getDayFromX(clientX: number): Date | null { + function minutesFromY(clientY: number): number { const config = getConfig(); - if (!config.containerEl) return null; - - const rect = config.containerEl.getBoundingClientRect(); - const relativeX = clientX - rect.left; - const dayWidth = rect.width / config.days.length; - const dayIndex = Math.floor(relativeX / dayWidth); - - if (dayIndex >= 0 && dayIndex < config.days.length) { - return config.days[dayIndex]; - } - return null; - } - - function getMinutesFromY(clientY: number): number { - const config = getConfig(); - if (!config.containerEl) return 0; - - const rect = config.containerEl.getBoundingClientRect(); - const scrollTop = config.containerEl.parentElement?.scrollTop || 0; - const relativeY = clientY - rect.top + scrollTop; - const visibleMinutes = - (relativeY / (config.totalVisibleHours * config.hourHeight)) * config.totalVisibleHours * 60; - const totalMinutes = visibleMinutes + config.firstVisibleHour * 60; - - const snap = getSnapMinutes(); - return Math.round(totalMinutes / snap) * snap; + return getMinutesFromY( + clientY, + config.containerEl, + config.totalVisibleHours, + config.hourHeight, + config.firstVisibleHour, + config.snapMinutes + ); } function updatePreview() { @@ -87,11 +68,11 @@ export function useDragToCreate(getConfig: () => DragToCreateConfig) { e.preventDefault(); - const day = getDayFromX(e.clientX); + const day = dayFromX(e.clientX); if (!day) return; - const minutes = getMinutesFromY(e.clientY); - const snap = getSnapMinutes(); + const minutes = minutesFromY(e.clientY); + const snap = getSnapMinutes(config.snapMinutes); const snappedMinutes = Math.round(minutes / snap) * snap; isCreating = true; @@ -111,12 +92,12 @@ export function useDragToCreate(getConfig: () => DragToCreateConfig) { hasMoved = true; const config = getConfig(); - const snap = getSnapMinutes(); + const snap = getSnapMinutes(config.snapMinutes); - const day = getDayFromX(e.clientX); + const day = dayFromX(e.clientX); if (day) createTargetDay = day; - const minutes = getMinutesFromY(e.clientY); + const minutes = minutesFromY(e.clientY); const snappedMinutes = Math.round(minutes / snap) * snap; if (snappedMinutes >= createStartMinutes) { @@ -156,8 +137,7 @@ export function useDragToCreate(getConfig: () => DragToCreateConfig) { } function getCreatePreviewTime(): string { - const pad = (n: number) => n.toString().padStart(2, '0'); - return `${pad(Math.floor(createStartMinutes / 60))}:${pad(createStartMinutes % 60)} - ${pad(Math.floor(createEndMinutes / 60))}:${pad(createEndMinutes % 60)}`; + return `${formatTime(Math.floor(createStartMinutes / 60), createStartMinutes % 60)} - ${formatTime(Math.floor(createEndMinutes / 60), createEndMinutes % 60)}`; } function cancel() { diff --git a/apps/calendar/apps/web/src/lib/composables/useEventDragDrop.svelte.ts b/apps/calendar/apps/web/src/lib/composables/useEventDragDrop.svelte.ts index 39c74a081..e31c2f93f 100644 --- a/apps/calendar/apps/web/src/lib/composables/useEventDragDrop.svelte.ts +++ b/apps/calendar/apps/web/src/lib/composables/useEventDragDrop.svelte.ts @@ -7,7 +7,7 @@ import type { CalendarEvent } from '@calendar/shared'; import { differenceInMinutes, addMinutes, setHours, setMinutes } from 'date-fns'; import { toDate } from '$lib/utils/eventDateHelpers'; import { eventsStore } from '$lib/stores/events.svelte'; -import { SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants'; +import { formatTime, getDayFromX, getMinutesFromY } from '$lib/utils/drag-helpers'; export interface EventDragDropConfig { /** Reference to the container element for position calculations */ @@ -69,51 +69,21 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { // ========== Helper Functions ========== - function getSnapMinutes(): number { - return getConfig().snapMinutes ?? SNAP_INTERVAL_MINUTES; - } - - function snapToGrid(minutes: number): number { - const snap = getSnapMinutes(); - return Math.round(minutes / snap) * snap; - } - - /** - * Get day from X coordinate (for multi-day views) - */ - function getDayFromX(clientX: number): Date | null { + function dayFromX(clientX: number): Date | null { const config = getConfig(); - if (!config.containerEl) return null; - - const rect = config.containerEl.getBoundingClientRect(); - const relativeX = clientX - rect.left; - const dayWidth = rect.width / config.days.length; - const dayIndex = Math.floor(relativeX / dayWidth); - - if (dayIndex >= 0 && dayIndex < config.days.length) { - return config.days[dayIndex]; - } - return null; + return getDayFromX(clientX, config.containerEl, config.days); } - /** - * Get minutes from Y coordinate - */ - function getMinutesFromY(clientY: number): number { + function minutesFromY(clientY: number): number { const config = getConfig(); - if (!config.containerEl) return 0; - - const rect = config.containerEl.getBoundingClientRect(); - const scrollTop = config.containerEl.parentElement?.scrollTop || 0; - const relativeY = clientY - rect.top + scrollTop; - - // Account for hidden early hours - const visibleMinutes = - (relativeY / (config.totalVisibleHours * config.hourHeight)) * config.totalVisibleHours * 60; - const totalMinutes = visibleMinutes + config.firstVisibleHour * 60; - - // Snap to interval - return snapToGrid(totalMinutes); + return getMinutesFromY( + clientY, + config.containerEl, + config.totalVisibleHours, + config.hourHeight, + config.firstVisibleHour, + config.snapMinutes + ); } // ========== Drag Functions ========== @@ -139,7 +109,7 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { dragTargetDay = start; // Calculate offset from event start to click position - const clickMinutes = getMinutesFromY(e.clientY); + const clickMinutes = minutesFromY(e.clientY); dragOffsetMinutes = clickMinutes - startMinutes; document.addEventListener('pointermove', handleDragMove); @@ -153,8 +123,8 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { hasMoved = true; // Calculate new position - const newDay = getDayFromX(e.clientX); - const newMinutes = getMinutesFromY(e.clientY) - dragOffsetMinutes; + const newDay = dayFromX(e.clientX); + const newMinutes = minutesFromY(e.clientY) - dragOffsetMinutes; // Clamp to valid range const clampedMinutes = Math.max( @@ -183,7 +153,7 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { const duration = differenceInMinutes(end, start); // Calculate new start time - const newMinutes = getMinutesFromY(e.clientY) - dragOffsetMinutes; + const newMinutes = minutesFromY(e.clientY) - dragOffsetMinutes; const clampedMinutes = Math.max(0, Math.min(24 * 60 - 15, newMinutes)); const newHours = Math.floor(clampedMinutes / 60); const newMins = clampedMinutes % 60; @@ -244,7 +214,7 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { resizePreviewHeight = (duration / (config.totalVisibleHours * 60)) * 100; // Calculate offset between snapped click position and actual event boundary - const clickMinutes = getMinutesFromY(e.clientY); + const clickMinutes = minutesFromY(e.clientY); if (edge === 'top') { resizeOffsetMinutes = clickMinutes - startMinutes; } else { @@ -261,7 +231,7 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { const config = getConfig(); hasMoved = true; - const currentMinutes = getMinutesFromY(e.clientY); + const currentMinutes = minutesFromY(e.clientY); // Apply offset to prevent jumping when drag starts const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = @@ -298,7 +268,7 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { } const config = getConfig(); - const currentMinutes = getMinutesFromY(e.clientY); + const currentMinutes = minutesFromY(e.clientY); // Apply offset to prevent jumping const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = @@ -399,8 +369,7 @@ export function useEventDragDrop(getConfig: () => EventDragDropConfig) { endMin = Math.round(previewEndMinutes); } - const pad = (n: number) => n.toString().padStart(2, '0'); - return `${pad(Math.floor(startMin / 60))}:${pad(startMin % 60)} - ${pad(Math.floor(endMin / 60))}:${pad(endMin % 60)}`; + return `${formatTime(Math.floor(startMin / 60), startMin % 60)} - ${formatTime(Math.floor(endMin / 60), endMin % 60)}`; } return { diff --git a/apps/calendar/apps/web/src/lib/composables/useSidebarDrop.svelte.ts b/apps/calendar/apps/web/src/lib/composables/useSidebarDrop.svelte.ts index b03aa49bc..5b975e7c0 100644 --- a/apps/calendar/apps/web/src/lib/composables/useSidebarDrop.svelte.ts +++ b/apps/calendar/apps/web/src/lib/composables/useSidebarDrop.svelte.ts @@ -5,7 +5,7 @@ import { todosStore } from '$lib/stores/todos.svelte'; import { format } from 'date-fns'; -import { SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants'; +import { formatTime, getSnapMinutes } from '$lib/utils/drag-helpers'; export interface SidebarDropConfig { /** First visible hour (for filtered hours mode) */ @@ -20,14 +20,6 @@ export function useSidebarDrop(getConfig: () => SidebarDropConfig) { // Track active drop target (for visual feedback) let dropTarget = $state<{ day: Date; y: number } | null>(null); - function getSnapMinutes(): number { - return getConfig().snapMinutes ?? SNAP_INTERVAL_MINUTES; - } - - function formatTime(hours: number, minutes: number): string { - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; - } - /** * Handle dragover event on a day column */ @@ -82,7 +74,7 @@ export function useSidebarDrop(getConfig: () => SidebarDropConfig) { const minutesPerPercent = (config.totalVisibleHours * 60) / 100; const rawMinutes = percentY * minutesPerPercent; - const snapMinutes = getSnapMinutes(); + const snapMinutes = getSnapMinutes(getConfig().snapMinutes); const snappedMinutes = Math.round(rawMinutes / snapMinutes) * snapMinutes; const totalMinutes = config.firstVisibleHour * 60 + snappedMinutes; diff --git a/apps/calendar/apps/web/src/lib/composables/useTaskDragDrop.svelte.ts b/apps/calendar/apps/web/src/lib/composables/useTaskDragDrop.svelte.ts index bef9100b6..d62aa4fe9 100644 --- a/apps/calendar/apps/web/src/lib/composables/useTaskDragDrop.svelte.ts +++ b/apps/calendar/apps/web/src/lib/composables/useTaskDragDrop.svelte.ts @@ -8,7 +8,7 @@ import type { Task } from '$lib/stores/todos.svelte'; import { todosStore } from '$lib/stores/todos.svelte'; import { format } from 'date-fns'; -import { SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants'; +import { formatTime, getSnapMinutes } from '$lib/utils/drag-helpers'; export interface TaskDragDropConfig { /** Reference to the container element for position calculations */ @@ -43,14 +43,6 @@ export function useTaskDragDrop(getConfig: () => TaskDragDropConfig) { // ========== Helper Functions ========== - function getSnapMinutes(): number { - return getConfig().snapMinutes ?? SNAP_INTERVAL_MINUTES; - } - - function formatTime(hours: number, minutes: number): string { - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; - } - // ========== Drag Functions ========== function startDrag(task: Task, e: PointerEvent) { @@ -105,7 +97,7 @@ export function useTaskDragDrop(getConfig: () => TaskDragDropConfig) { // Snap to intervals const minutesPerPercent = (config.totalVisibleHours * 60) / 100; const rawMinutes = percentY * minutesPerPercent; - const snapMinutes = getSnapMinutes(); + const snapMinutes = getSnapMinutes(getConfig().snapMinutes); const snappedMinutes = Math.round(rawMinutes / snapMinutes) * snapMinutes; taskDragPreviewTop = (snappedMinutes / (config.totalVisibleHours * 60)) * 100; } @@ -192,7 +184,7 @@ export function useTaskDragDrop(getConfig: () => TaskDragDropConfig) { const percentY = Math.max(0, Math.min(100, (relativeY / rect.height) * 100)); const minutesPerPercent = (config.totalVisibleHours * 60) / 100; - const snapMinutes = getSnapMinutes(); + const snapMinutes = getSnapMinutes(getConfig().snapMinutes); if (taskResizeEdge === 'top') { // Adjust start time, keep end fixed diff --git a/apps/calendar/apps/web/src/lib/utils/drag-helpers.ts b/apps/calendar/apps/web/src/lib/utils/drag-helpers.ts new file mode 100644 index 000000000..08bb26f78 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/utils/drag-helpers.ts @@ -0,0 +1,73 @@ +/** + * Shared drag/drop utility functions + * Used by useEventDragDrop, useDragToCreate + */ + +import { SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants'; + +/** + * Format hours and minutes as HH:MM string + */ +export function formatTime(hours: number, minutes: number): string { + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; +} + +/** + * Get the effective snap interval, falling back to the default constant + */ +export function getSnapMinutes(snapMinutes?: number): number { + return snapMinutes ?? SNAP_INTERVAL_MINUTES; +} + +/** + * Snap a minute value to the nearest grid interval + */ +export function snapToGrid(minutes: number, snapMinutes?: number): number { + const snap = getSnapMinutes(snapMinutes); + return Math.round(minutes / snap) * snap; +} + +/** + * Map an X client coordinate to a day column based on container width + */ +export function getDayFromX( + clientX: number, + containerEl: HTMLElement | null, + days: Date[] +): Date | null { + if (!containerEl) return null; + + const rect = containerEl.getBoundingClientRect(); + const relativeX = clientX - rect.left; + const dayWidth = rect.width / days.length; + const dayIndex = Math.floor(relativeX / dayWidth); + + if (dayIndex >= 0 && dayIndex < days.length) { + return days[dayIndex]; + } + return null; +} + +/** + * Map a Y client coordinate to total minutes in the day, + * accounting for scroll offset, visible hour range, and snap interval + */ +export function getMinutesFromY( + clientY: number, + containerEl: HTMLElement | null, + totalVisibleHours: number, + hourHeight: number, + firstVisibleHour: number, + snapMinutes?: number +): number { + if (!containerEl) return 0; + + const rect = containerEl.getBoundingClientRect(); + const scrollTop = containerEl.parentElement?.scrollTop || 0; + const relativeY = clientY - rect.top + scrollTop; + + const visibleMinutes = (relativeY / (totalVisibleHours * hourHeight)) * totalVisibleHours * 60; + const totalMinutes = visibleMinutes + firstVisibleHour * 60; + + return snapToGrid(totalMinutes, snapMinutes); +} diff --git a/apps/todo/apps/web/src/lib/components/TaskItem.svelte b/apps/todo/apps/web/src/lib/components/TaskItem.svelte index bc1cb0c2c..6bb718035 100644 --- a/apps/todo/apps/web/src/lib/components/TaskItem.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskItem.svelte @@ -9,8 +9,8 @@ } from '@todo/shared'; import type { ContactReference, ContactOrManual } from '@manacore/shared-types'; import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared'; - import { format, isToday, isPast, isTomorrow } from 'date-fns'; - import { de } from 'date-fns/locale'; + import { isToday, isPast } from 'date-fns'; + import { formatDueDate } from '$lib/utils/date-display'; import { getContext } from 'svelte'; import type { Project } from '@todo/shared'; import { getActiveProjects, getProjectColor } from '$lib/data/task-queries'; @@ -27,6 +27,7 @@ FunRatingPicker, TagSelector, } from './form'; + import { PRIORITY_COLORS } from '$lib/constants/priority'; interface Props { task: Task; @@ -291,21 +292,12 @@ subtasks = newSubtasks; } - // Priority colors - const priorityColors: Record = { - low: '#22c55e', - medium: '#eab308', - high: '#f97316', - urgent: '#ef4444', - }; + const priorityColors = PRIORITY_COLORS; // Format due date let dueDateText = $derived(() => { if (!task.dueDate) return null; - const date = new Date(task.dueDate); - if (isToday(date)) return 'Heute'; - if (isTomorrow(date)) return 'Morgen'; - return format(date, 'dd. MMM', { locale: de }); + return formatDueDate(new Date(task.dueDate)); }); // Check if overdue diff --git a/apps/todo/apps/web/src/lib/components/kanban/KanbanTaskCard.svelte b/apps/todo/apps/web/src/lib/components/kanban/KanbanTaskCard.svelte index 535d27cc1..4be319555 100644 --- a/apps/todo/apps/web/src/lib/components/kanban/KanbanTaskCard.svelte +++ b/apps/todo/apps/web/src/lib/components/kanban/KanbanTaskCard.svelte @@ -1,7 +1,7 @@