mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 09:06:06 +00:00
356 lines
9.9 KiB
JavaScript
356 lines
9.9 KiB
JavaScript
import { useCallback, useEffect } from 'react';
|
|
import dayjs from 'dayjs';
|
|
import duration from 'dayjs/plugin/duration';
|
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
import utc from 'dayjs/plugin/utc';
|
|
import timezone from 'dayjs/plugin/timezone';
|
|
import useSettingsStore from '../store/settings';
|
|
import useLocalStorage from '../hooks/useLocalStorage';
|
|
|
|
dayjs.extend(duration);
|
|
dayjs.extend(relativeTime);
|
|
dayjs.extend(utc);
|
|
dayjs.extend(timezone);
|
|
|
|
export const convertToMs = (dateTime) => dayjs(dateTime).valueOf();
|
|
|
|
export const convertToSec = (dateTime) => dayjs(dateTime).unix();
|
|
|
|
export const initializeTime = (dateTime) => dayjs(dateTime);
|
|
|
|
export const startOfDay = (dateTime) => dayjs(dateTime).startOf('day');
|
|
|
|
export const isBefore = (date1, date2) => dayjs(date1).isBefore(date2);
|
|
|
|
export const isAfter = (date1, date2) => dayjs(date1).isAfter(date2);
|
|
|
|
export const isSame = (date1, date2, unit = 'day') =>
|
|
dayjs(date1).isSame(date2, unit);
|
|
|
|
export const add = (dateTime, value, unit) => dayjs(dateTime).add(value, unit);
|
|
|
|
export const subtract = (dateTime, value, unit) =>
|
|
dayjs(dateTime).subtract(value, unit);
|
|
|
|
export const diff = (date1, date2, unit = 'millisecond') =>
|
|
dayjs(date1).diff(date2, unit);
|
|
|
|
export const format = (dateTime, formatStr) =>
|
|
dayjs(dateTime).format(formatStr);
|
|
|
|
export const getNow = () => dayjs();
|
|
|
|
export const toFriendlyDuration = (dateTime, unit) =>
|
|
dayjs.duration(dateTime, unit).humanize();
|
|
|
|
export const formatExactDuration = (seconds) => {
|
|
if (seconds < 60) return `${seconds.toFixed(1)} seconds`;
|
|
if (seconds < 3600) {
|
|
const m = Math.floor(seconds / 60);
|
|
const s = Math.floor(seconds % 60);
|
|
return `${m} minute${m !== 1 ? 's' : ''}, ${s} second${s !== 1 ? 's' : ''}`;
|
|
}
|
|
if (seconds < 86400) {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
return `${h} hour${h !== 1 ? 's' : ''}, ${m} minute${m !== 1 ? 's' : ''}`;
|
|
}
|
|
const d = Math.floor(seconds / 86400);
|
|
const h = Math.floor((seconds % 86400) / 3600);
|
|
return `${d} day${d !== 1 ? 's' : ''}, ${h} hour${h !== 1 ? 's' : ''}`;
|
|
};
|
|
|
|
export const fromNow = (dateTime) => dayjs(dateTime).fromNow();
|
|
|
|
export const setTz = (dateTime, timeZone) => dayjs(dateTime).tz(timeZone);
|
|
|
|
export const setMonth = (dateTime, value) => dayjs(dateTime).month(value);
|
|
|
|
export const setYear = (dateTime, value) => dayjs(dateTime).year(value);
|
|
|
|
export const setDay = (dateTime, value) => dayjs(dateTime).date(value);
|
|
|
|
export const setHour = (dateTime, value) => dayjs(dateTime).hour(value);
|
|
|
|
export const setMinute = (dateTime, value) => dayjs(dateTime).minute(value);
|
|
|
|
export const setSecond = (dateTime, value) => dayjs(dateTime).second(value);
|
|
|
|
export const getMonth = (dateTime) => dayjs(dateTime).month();
|
|
|
|
export const getYear = (dateTime) => dayjs(dateTime).year();
|
|
|
|
export const getDay = (dateTime) => dayjs(dateTime).date();
|
|
|
|
export const getHour = (dateTime) => dayjs(dateTime).hour();
|
|
|
|
export const getMinute = (dateTime) => dayjs(dateTime).minute();
|
|
|
|
export const getSecond = (dateTime) => dayjs(dateTime).second();
|
|
|
|
export const getNowMs = () => Date.now();
|
|
|
|
export const roundToNearest = (dateTime, minutes) => {
|
|
const current = initializeTime(dateTime);
|
|
const minute = current.minute();
|
|
const snappedMinute = Math.round(minute / minutes) * minutes;
|
|
|
|
return snappedMinute === 60
|
|
? current.add(1, 'hour').minute(0)
|
|
: current.minute(snappedMinute);
|
|
};
|
|
|
|
export const useUserTimeZone = () => {
|
|
const settings = useSettingsStore((s) => s.settings);
|
|
const [timeZone, setTimeZone] = useLocalStorage(
|
|
'time-zone',
|
|
dayjs.tz?.guess
|
|
? dayjs.tz.guess()
|
|
: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
);
|
|
|
|
useEffect(() => {
|
|
const tz = settings?.['system_settings']?.value?.time_zone;
|
|
if (tz && tz !== timeZone) {
|
|
setTimeZone(tz);
|
|
}
|
|
}, [settings, timeZone, setTimeZone]);
|
|
|
|
return timeZone;
|
|
};
|
|
|
|
export const useTimeHelpers = () => {
|
|
const timeZone = useUserTimeZone();
|
|
|
|
const toUserTime = useCallback(
|
|
(value) => {
|
|
if (!value) return dayjs(null);
|
|
try {
|
|
return initializeTime(value).tz(timeZone);
|
|
} catch (error) {
|
|
return initializeTime(value);
|
|
}
|
|
},
|
|
[timeZone]
|
|
);
|
|
|
|
const userNow = useCallback(() => getNow().tz(timeZone), [timeZone]);
|
|
|
|
return { timeZone, toUserTime, userNow };
|
|
};
|
|
|
|
export const RECURRING_DAY_OPTIONS = [
|
|
{ value: 6, label: 'Sun' },
|
|
{ value: 0, label: 'Mon' },
|
|
{ value: 1, label: 'Tue' },
|
|
{ value: 2, label: 'Wed' },
|
|
{ value: 3, label: 'Thu' },
|
|
{ value: 4, label: 'Fri' },
|
|
{ value: 5, label: 'Sat' },
|
|
];
|
|
|
|
export const useDateTimeFormat = () => {
|
|
const [timeFormatSetting] = useLocalStorage('time-format', '12h');
|
|
const [dateFormatSetting] = useLocalStorage('date-format', 'mdy');
|
|
// Use user preference for time format
|
|
const timeFormat = timeFormatSetting === '12h' ? 'h:mma' : 'HH:mm';
|
|
const dateFormat = dateFormatSetting === 'mdy' ? 'MMM D' : 'D MMM';
|
|
|
|
// Full format strings for detailed date-time displays
|
|
const fullDateFormat =
|
|
dateFormatSetting === 'mdy' ? 'MM/DD/YYYY' : 'DD/MM/YYYY';
|
|
const fullTimeFormat = timeFormatSetting === '12h' ? 'h:mm:ss A' : 'HH:mm:ss';
|
|
const fullDateTimeFormat = `${fullDateFormat}, ${fullTimeFormat}`;
|
|
|
|
return {
|
|
timeFormat,
|
|
dateFormat,
|
|
fullDateFormat,
|
|
fullTimeFormat,
|
|
fullDateTimeFormat,
|
|
// Also return raw settings for cases that need them
|
|
timeFormatSetting,
|
|
dateFormatSetting,
|
|
};
|
|
};
|
|
|
|
export const toTimeString = (value) => {
|
|
if (!value) return '00:00';
|
|
if (typeof value === 'string') {
|
|
const parsed = dayjs(value, ['HH:mm', 'HH:mm:ss', 'h:mm A'], true);
|
|
if (parsed.isValid()) return parsed.format('HH:mm');
|
|
return value;
|
|
}
|
|
const parsed = initializeTime(value);
|
|
return parsed.isValid() ? parsed.format('HH:mm') : '00:00';
|
|
};
|
|
|
|
export const parseDate = (value) => {
|
|
if (!value) return null;
|
|
const parsed = dayjs(value, ['YYYY-MM-DD', dayjs.ISO_8601], true);
|
|
return parsed.isValid() ? parsed.toDate() : null;
|
|
};
|
|
|
|
const TIMEZONE_FALLBACKS = [
|
|
'UTC',
|
|
'America/New_York',
|
|
'America/Chicago',
|
|
'America/Denver',
|
|
'America/Los_Angeles',
|
|
'America/Phoenix',
|
|
'America/Anchorage',
|
|
'Pacific/Honolulu',
|
|
'Europe/London',
|
|
'Europe/Paris',
|
|
'Europe/Berlin',
|
|
'Europe/Madrid',
|
|
'Europe/Warsaw',
|
|
'Europe/Moscow',
|
|
'Asia/Dubai',
|
|
'Asia/Kolkata',
|
|
'Asia/Shanghai',
|
|
'Asia/Tokyo',
|
|
'Asia/Seoul',
|
|
'Australia/Sydney',
|
|
];
|
|
|
|
const getSupportedTimeZones = () => {
|
|
try {
|
|
if (typeof Intl.supportedValuesOf === 'function') {
|
|
return Intl.supportedValuesOf('timeZone');
|
|
}
|
|
} catch (error) {
|
|
console.warn('Unable to enumerate supported time zones:', error);
|
|
}
|
|
return TIMEZONE_FALLBACKS;
|
|
};
|
|
|
|
const getTimeZoneOffsetMinutes = (date, timeZone) => {
|
|
try {
|
|
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
timeZone,
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hourCycle: 'h23',
|
|
});
|
|
const parts = dtf.formatToParts(date).reduce((acc, part) => {
|
|
if (part.type !== 'literal') acc[part.type] = part.value;
|
|
return acc;
|
|
}, {});
|
|
const asUTC = Date.UTC(
|
|
Number(parts.year),
|
|
Number(parts.month) - 1,
|
|
Number(parts.day),
|
|
Number(parts.hour),
|
|
Number(parts.minute),
|
|
Number(parts.second)
|
|
);
|
|
return (asUTC - date.getTime()) / 60000;
|
|
} catch (error) {
|
|
console.warn(`Failed to compute offset for ${timeZone}:`, error);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
const formatOffset = (minutes) => {
|
|
const rounded = Math.round(minutes);
|
|
const sign = rounded < 0 ? '-' : '+';
|
|
const absolute = Math.abs(rounded);
|
|
const hours = String(Math.floor(absolute / 60)).padStart(2, '0');
|
|
const mins = String(absolute % 60).padStart(2, '0');
|
|
return `UTC${sign}${hours}:${mins}`;
|
|
};
|
|
|
|
export const buildTimeZoneOptions = (preferredZone) => {
|
|
const zones = getSupportedTimeZones();
|
|
const referenceYear = new Date().getUTCFullYear();
|
|
const janDate = new Date(Date.UTC(referenceYear, 0, 1, 12, 0, 0));
|
|
const julDate = new Date(Date.UTC(referenceYear, 6, 1, 12, 0, 0));
|
|
|
|
const options = zones
|
|
.map((zone) => {
|
|
const janOffset = getTimeZoneOffsetMinutes(janDate, zone);
|
|
const julOffset = getTimeZoneOffsetMinutes(julDate, zone);
|
|
const currentOffset = getTimeZoneOffsetMinutes(new Date(), zone);
|
|
const minOffset = Math.min(janOffset, julOffset);
|
|
const maxOffset = Math.max(janOffset, julOffset);
|
|
const usesDst = minOffset !== maxOffset;
|
|
const labelParts = [`now ${formatOffset(currentOffset)}`];
|
|
if (usesDst) {
|
|
labelParts.push(
|
|
`DST range ${formatOffset(minOffset)} to ${formatOffset(maxOffset)}`
|
|
);
|
|
}
|
|
return {
|
|
value: zone,
|
|
label: `${zone} (${labelParts.join(' | ')})`,
|
|
numericOffset: minOffset,
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
if (a.numericOffset !== b.numericOffset) {
|
|
return a.numericOffset - b.numericOffset;
|
|
}
|
|
return a.value.localeCompare(b.value);
|
|
});
|
|
if (
|
|
preferredZone &&
|
|
!options.some((option) => option.value === preferredZone)
|
|
) {
|
|
const currentOffset = getTimeZoneOffsetMinutes(new Date(), preferredZone);
|
|
options.push({
|
|
value: preferredZone,
|
|
label: `${preferredZone} (now ${formatOffset(currentOffset)})`,
|
|
numericOffset: currentOffset,
|
|
});
|
|
options.sort((a, b) => {
|
|
if (a.numericOffset !== b.numericOffset) {
|
|
return a.numericOffset - b.numericOffset;
|
|
}
|
|
return a.value.localeCompare(b.value);
|
|
});
|
|
}
|
|
return options;
|
|
};
|
|
|
|
export const getDefaultTimeZone = () => {
|
|
try {
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
} catch (error) {
|
|
return 'UTC';
|
|
}
|
|
};
|
|
|
|
export const MONTH_NAMES = [
|
|
'january',
|
|
'february',
|
|
'march',
|
|
'april',
|
|
'may',
|
|
'june',
|
|
'july',
|
|
'august',
|
|
'september',
|
|
'october',
|
|
'november',
|
|
'december',
|
|
];
|
|
|
|
export const MONTH_ABBR = [
|
|
'jan',
|
|
'feb',
|
|
'mar',
|
|
'apr',
|
|
'may',
|
|
'jun',
|
|
'jul',
|
|
'aug',
|
|
'sep',
|
|
'oct',
|
|
'nov',
|
|
'dec',
|
|
];
|