mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-05-07 20:32:26 +00:00
Fix DST-related timezone offset when creating/resizing/dragging calendar
events
This commit is contained in:
@@ -91,9 +91,7 @@ const emit = defineEmits<{
|
||||
:aria-label="dayEvent.event.title"
|
||||
role="button"
|
||||
@pointerdown="emit('event-pointerdown', $event, dayEvent)"
|
||||
@keydown.enter.prevent="
|
||||
!dayEvent.event.isRunning && emit('event-keydown-enter', dayEvent)
|
||||
">
|
||||
@keydown.enter.prevent="emit('event-keydown-enter', dayEvent)">
|
||||
<div
|
||||
v-if="!dayEvent.isClippedStart"
|
||||
class="fc-event-resizer fc-event-resizer-start absolute z-[99] w-full h-3 left-0 top-[-2px] cursor-row-resize flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ref, type Ref, type ComputedRef } from 'vue';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { TimeEntry } from '@/packages/api/src';
|
||||
import { getDayJsInstance } from '../utils/time';
|
||||
import { getUserTimezone } from '../utils/settings';
|
||||
import { getDayJsInstance, getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||
|
||||
import type { CalendarSettings } from './calendarSettings';
|
||||
import type { CalendarEvent } from './calendarTypes';
|
||||
|
||||
@@ -34,11 +34,8 @@ export function useContextMenu(params: {
|
||||
const snap = params.calendarSettings.value.snapMinutes;
|
||||
const snappedMinutes = Math.floor(minutesFromGridStart / snap) * snap;
|
||||
|
||||
const dayjs = getDayJsInstance();
|
||||
const startLocal = dayjs(`${date}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(snappedMinutes, 'minute');
|
||||
const snappedEnd = startLocal.add(snap, 'minute');
|
||||
const startLocal = getLocalizedDayJsFromMinutes(date, snappedMinutes);
|
||||
const snappedEnd = getLocalizedDayJsFromMinutes(date, snappedMinutes + snap);
|
||||
|
||||
return { start: startLocal.utc(), end: snappedEnd.utc() };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { TimeEntry } from '@/packages/api/src';
|
||||
import { getDayJsInstance, getLocalizedDayJs } from '../utils/time';
|
||||
import { getUserTimezone } from '../utils/settings';
|
||||
import { getLocalizedDayJs, getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||
import type { CalendarSettings } from './calendarSettings';
|
||||
import type { CalendarEvent, DayEvent } from './calendarTypes';
|
||||
import { SLOT_HEIGHT, DRAG_THRESHOLD } from './calendarTypes';
|
||||
@@ -67,8 +66,7 @@ export function useEventDrag(params: {
|
||||
}
|
||||
|
||||
if (dayEvent.isClippedStart && originDay && ev.timeEntry.end) {
|
||||
const dayjs = getDayJsInstance();
|
||||
const dayMidnight = dayjs(`${originDay}T00:00:00`).tz(getUserTimezone(), true);
|
||||
const dayMidnight = getLocalizedDayJsFromMinutes(originDay, 0);
|
||||
const evStart = getLocalizedDayJs(ev.timeEntry.start);
|
||||
const eventStartFromGridStart = evStart.diff(dayMidnight, 'minute') - s.startHour * 60;
|
||||
const segmentTopMinutes = (dayEvent.top / SLOT_HEIGHT) * s.slotMinutes;
|
||||
@@ -154,13 +152,11 @@ export function useEventDrag(params: {
|
||||
const lowerBound = startMin - 4 * 60;
|
||||
const clampedMinutes = Math.max(lowerBound, Math.min(snappedMinutes, s.endHour * 60));
|
||||
|
||||
const dayjs = getDayJsInstance();
|
||||
const originalSegmentStart = dayjs(`${savedOriginalDayStr}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(startMin + params.pixelsToMinutesFromMidnight(dragStartEventTop), 'minute');
|
||||
const newSegmentStart = dayjs(`${targetDateStr}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(clampedMinutes, 'minute');
|
||||
const originalSegmentStart = getLocalizedDayJsFromMinutes(
|
||||
savedOriginalDayStr,
|
||||
startMin + params.pixelsToMinutesFromMidnight(dragStartEventTop)
|
||||
);
|
||||
const newSegmentStart = getLocalizedDayJsFromMinutes(targetDateStr, clampedMinutes);
|
||||
const deltaMs = newSegmentStart.diff(originalSegmentStart);
|
||||
|
||||
const origStart = getLocalizedDayJs(timeEntry.start);
|
||||
@@ -240,11 +236,14 @@ export function useEventDrag(params: {
|
||||
}
|
||||
|
||||
// Multi-day: compute actual start/end datetimes, then clip per day
|
||||
const dayjs = getDayJsInstance();
|
||||
const eventStartAbsolute = dayjs(`${dragCurrentDay.value}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(startMin + eventStartOnGrid, 'minute');
|
||||
const eventEndAbsolute = eventStartAbsolute.add(dragFullDurationMinutes, 'minute');
|
||||
const eventStartAbsolute = getLocalizedDayJsFromMinutes(
|
||||
dragCurrentDay.value,
|
||||
startMin + eventStartOnGrid
|
||||
);
|
||||
const eventEndAbsolute = getLocalizedDayJsFromMinutes(
|
||||
dragCurrentDay.value,
|
||||
startMin + eventStartOnGrid + dragFullDurationMinutes
|
||||
);
|
||||
|
||||
const result: Record<string, Record<string, string>> = {};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { TimeEntry } from '@/packages/api/src';
|
||||
import { getDayJsInstance, getLocalizedDayJs } from '../utils/time';
|
||||
import { getUserTimezone } from '../utils/settings';
|
||||
import { getDayJsInstance, getLocalizedDayJs, getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||
import type { CalendarSettings } from './calendarSettings';
|
||||
import type { CalendarEvent, DayEvent } from './calendarTypes';
|
||||
import { SLOT_HEIGHT } from './calendarTypes';
|
||||
@@ -11,10 +10,6 @@ function snapTo(value: number, step: number): number {
|
||||
return Math.round(value / step) * step;
|
||||
}
|
||||
|
||||
function dayMidnightLocal(dayStr: string): Dayjs {
|
||||
return getDayJsInstance()(`${dayStr}T00:00:00`).tz(getUserTimezone(), true);
|
||||
}
|
||||
|
||||
export function useEventResize(params: {
|
||||
calendarSettings: Ref<CalendarSettings>;
|
||||
viewDays: ComputedRef<Dayjs[]>;
|
||||
@@ -89,7 +84,7 @@ export function useEventResize(params: {
|
||||
),
|
||||
s.snapMinutes
|
||||
);
|
||||
return { start, end: dayMidnightLocal(endDay).add(endMinutes, 'minute') };
|
||||
return { start, end: getLocalizedDayJsFromMinutes(endDay, endMinutes) };
|
||||
} else {
|
||||
const end = resizeOriginalEvent.isRunning
|
||||
? getLocalizedDayJs()
|
||||
@@ -105,7 +100,7 @@ export function useEventResize(params: {
|
||||
params.pixelsToMinutesFromMidnight(resizeCurrentTop.value),
|
||||
s.snapMinutes
|
||||
);
|
||||
return { start: dayMidnightLocal(startDay).add(startMinutes, 'minute'), end };
|
||||
return { start: getLocalizedDayJsFromMinutes(startDay, startMinutes), end };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { getDayJsInstance } from '../utils/time';
|
||||
import { getUserTimezone } from '../utils/settings';
|
||||
import { getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||
|
||||
import type { CalendarSettings } from './calendarSettings';
|
||||
import { SLOT_HEIGHT } from './calendarTypes';
|
||||
|
||||
@@ -102,8 +102,6 @@ export function useSlotSelection(params: {
|
||||
|
||||
const s = params.calendarSettings.value;
|
||||
const snap = s.snapMinutes;
|
||||
const dayjs = getDayJsInstance();
|
||||
|
||||
const startMinutes = params.pixelsToMinutesFromMidnight(selectionTop.value);
|
||||
const snappedStartMin = Math.floor(startMinutes / snap) * snap;
|
||||
|
||||
@@ -138,12 +136,8 @@ export function useSlotSelection(params: {
|
||||
if (endMin <= 0) endMin = snap;
|
||||
}
|
||||
|
||||
startLocal = dayjs(`${startDateStr}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(startMin, 'minute');
|
||||
endLocal = dayjs(`${endDateStr}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(endMin, 'minute');
|
||||
startLocal = getLocalizedDayJsFromMinutes(startDateStr, startMin);
|
||||
endLocal = getLocalizedDayJsFromMinutes(endDateStr, endMin);
|
||||
} else {
|
||||
const startDateStr = selectionStartDay;
|
||||
const endMinutes = params.pixelsToMinutesFromMidnight(
|
||||
@@ -153,12 +147,8 @@ export function useSlotSelection(params: {
|
||||
if (snappedEndMin <= snappedStartMin) {
|
||||
snappedEndMin = snappedStartMin + snap;
|
||||
}
|
||||
startLocal = dayjs(`${startDateStr}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(snappedStartMin, 'minute');
|
||||
endLocal = dayjs(`${startDateStr}T00:00:00`)
|
||||
.tz(getUserTimezone(), true)
|
||||
.add(snappedEndMin, 'minute');
|
||||
startLocal = getLocalizedDayJsFromMinutes(startDateStr, snappedStartMin);
|
||||
endLocal = getLocalizedDayJsFromMinutes(startDateStr, snappedEndMin);
|
||||
}
|
||||
|
||||
params.onSelectionComplete(startLocal.utc(), endLocal.utc());
|
||||
|
||||
@@ -147,6 +147,18 @@ export function getLocalizedDayJs(timestamp?: string | null) {
|
||||
return dayjs.utc(timestamp).tz(getUserTimezone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dayjs instance for a specific wall-clock time on a given day.
|
||||
* Sets hour/minute directly to avoid DST issues with .add() on transition days.
|
||||
*/
|
||||
export function getLocalizedDayJsFromMinutes(dayStr: string, minutesFromMidnight: number) {
|
||||
return dayjs
|
||||
.tz(`${dayStr}T00:00:00`, getUserTimezone())
|
||||
.hour(Math.floor(minutesFromMidnight / 60))
|
||||
.minute(Math.round(minutesFromMidnight % 60))
|
||||
.second(0);
|
||||
}
|
||||
|
||||
export function getLocalizedDateFromTimestamp(timestamp: string) {
|
||||
return getLocalizedDayJs(timestamp).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user