mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
refactor(todo,calendar): extract duplicated constants and utilities
Todo: - Extract priority colors to lib/constants/priority.ts (was in TaskItem + KanbanTaskCard) - Extract formatDueDate to lib/utils/date-display.ts (was in TaskItem + KanbanTaskCard + task-parser) - Extract withErrorHandling to lib/stores/store-helpers.ts (was in 3 stores) Calendar: - Extract formatTime, snapToGrid, getDayFromX, getMinutesFromY to lib/utils/drag-helpers.ts (was duplicated across 4 drag/drop composables) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
74ff066050
commit
101f20ec34
13 changed files with 438 additions and 371 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
73
apps/calendar/apps/web/src/lib/utils/drag-helpers.ts
Normal file
73
apps/calendar/apps/web/src/lib/utils/drag-helpers.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue