cynx-lib 1.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +3 -0
- package/package.json +36 -0
- package/ui/components/ActionsMenu.jsx +158 -0
- package/ui/components/ActivityCard.jsx +184 -0
- package/ui/components/AuthOverlay.jsx +94 -0
- package/ui/components/Badge.jsx +186 -0
- package/ui/components/BrandingCard.jsx +146 -0
- package/ui/components/Button.jsx +148 -0
- package/ui/components/Card.jsx +132 -0
- package/ui/components/Checkbox.jsx +59 -0
- package/ui/components/ChipSelect.jsx +54 -0
- package/ui/components/CircularProgress.jsx +216 -0
- package/ui/components/Confirm.jsx +79 -0
- package/ui/components/DatePicker.jsx +605 -0
- package/ui/components/Divider.jsx +96 -0
- package/ui/components/Dropzone.jsx +203 -0
- package/ui/components/FilePicker.jsx +205 -0
- package/ui/components/HoverCard.jsx +81 -0
- package/ui/components/ImageUpload.jsx +147 -0
- package/ui/components/InfoBadge.jsx +204 -0
- package/ui/components/Input.jsx +229 -0
- package/ui/components/LineProgress.jsx +133 -0
- package/ui/components/LinearProgress.jsx +132 -0
- package/ui/components/Modal.jsx +212 -0
- package/ui/components/Pagination.jsx +52 -0
- package/ui/components/PulseDot.jsx +77 -0
- package/ui/components/Select.jsx +415 -0
- package/ui/components/Skeleton.jsx +118 -0
- package/ui/components/Spinner.jsx +108 -0
- package/ui/components/StatBarCard.jsx +25 -0
- package/ui/components/StatsBar.jsx +88 -0
- package/ui/components/StatsCard.jsx +171 -0
- package/ui/components/TabBar.jsx +404 -0
- package/ui/components/Tabs.jsx +29 -0
- package/ui/components/TimePicker.jsx +1069 -0
- package/ui/components/Toast.jsx +106 -0
- package/ui/components/Toggle.jsx +67 -0
- package/ui/components/UptimeBar.jsx +135 -0
- package/ui/components/XTable.jsx +44 -0
- package/ui/index.js +97 -0
- package/utils/index.js +11 -0
- package/utils/utils/oidc.js +209 -0
- package/utils/utils/realtime.js +1040 -0
- package/utils/utils/storage.js +393 -0
- package/utils/utils/tz.js +625 -0
- package/utils/utils/uid.js +252 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cynx/utils — Universal Time Zone & Date Engine (tz.js)
|
|
3
|
+
*
|
|
4
|
+
* Lightweight, zero-dependency timezone and date utility powered by native Intl API.
|
|
5
|
+
* Compatible with Browser, Node.js, Web Workers, and SSR.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── CACHE & HELPERS ────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const formatterCache = new Map();
|
|
11
|
+
|
|
12
|
+
function getCachedFormatter(locale, options) {
|
|
13
|
+
const key = `${locale}_${JSON.stringify(options)}`;
|
|
14
|
+
if (!formatterCache.has(key)) {
|
|
15
|
+
formatterCache.set(key, new Intl.DateTimeFormat(locale, options));
|
|
16
|
+
}
|
|
17
|
+
return formatterCache.get(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseDate(input) {
|
|
21
|
+
if (input instanceof Date) return new Date(input.getTime());
|
|
22
|
+
if (typeof input === 'number') return new Date(input);
|
|
23
|
+
if (typeof input === 'string') {
|
|
24
|
+
const d = new Date(input);
|
|
25
|
+
if (!isNaN(d.getTime())) return d;
|
|
26
|
+
}
|
|
27
|
+
return new Date();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Left-pad a number with leading zeros.
|
|
32
|
+
*
|
|
33
|
+
* @param {number|string} n
|
|
34
|
+
* @param {number} [length=2]
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function pad(n, length = 2) {
|
|
38
|
+
return String(n).padStart(length, '0');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── 1. TIMEZONE DISCOVERY & VALIDATION ──────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the current user/system IANA timezone.
|
|
45
|
+
* Example: "Asia/Tashkent", "Europe/London", "America/New_York"
|
|
46
|
+
*
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function getUserTimezone() {
|
|
50
|
+
try {
|
|
51
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
52
|
+
} catch {
|
|
53
|
+
return 'UTC';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a given string is a valid IANA timezone name.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} timezone
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function isValidTimezone(timezone) {
|
|
64
|
+
if (!timezone || typeof timezone !== 'string') return false;
|
|
65
|
+
try {
|
|
66
|
+
Intl.DateTimeFormat(undefined, { timeZone: timezone });
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── 2. TIMEZONE CONVERSION & OFFSETS ───────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract date/time parts for a given date in a specific timezone.
|
|
77
|
+
*
|
|
78
|
+
* @param {Date|number|string} date
|
|
79
|
+
* @param {string} [timezone] Target IANA timezone (default: user timezone)
|
|
80
|
+
* @returns {{ year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, weekday: number }}
|
|
81
|
+
*/
|
|
82
|
+
export function getTimezoneParts(date, timezone = getUserTimezone()) {
|
|
83
|
+
const d = parseDate(date);
|
|
84
|
+
const formatter = getCachedFormatter('en-US', {
|
|
85
|
+
timeZone: timezone,
|
|
86
|
+
year: 'numeric',
|
|
87
|
+
month: 'numeric',
|
|
88
|
+
day: 'numeric',
|
|
89
|
+
hour: 'numeric',
|
|
90
|
+
minute: 'numeric',
|
|
91
|
+
second: 'numeric',
|
|
92
|
+
fractionalSecondDigits: 3,
|
|
93
|
+
hour12: false,
|
|
94
|
+
weekday: 'short',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const parts = formatter.formatToParts(d);
|
|
98
|
+
const result = {
|
|
99
|
+
year: 0,
|
|
100
|
+
month: 1,
|
|
101
|
+
day: 1,
|
|
102
|
+
hour: 0,
|
|
103
|
+
minute: 0,
|
|
104
|
+
second: 0,
|
|
105
|
+
millisecond: d.getMilliseconds(),
|
|
106
|
+
weekday: d.getDay(),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
for (const { type, value } of parts) {
|
|
110
|
+
if (type === 'year') result.year = parseInt(value, 10);
|
|
111
|
+
else if (type === 'month') result.month = parseInt(value, 10);
|
|
112
|
+
else if (type === 'day') result.day = parseInt(value, 10);
|
|
113
|
+
else if (type === 'hour') result.hour = parseInt(value, 10) === 24 ? 0 : parseInt(value, 10);
|
|
114
|
+
else if (type === 'minute') result.minute = parseInt(value, 10);
|
|
115
|
+
else if (type === 'second') result.second = parseInt(value, 10);
|
|
116
|
+
else if (type === 'fractionalSecond') result.millisecond = parseInt(value, 10);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the UTC offset for a specific timezone and date.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} [timezone] Target IANA timezone
|
|
126
|
+
* @param {Date|number|string} [date] Reference date
|
|
127
|
+
* @returns {{ offsetMinutes: number, offsetString: string, formatted: string }}
|
|
128
|
+
* Example: { offsetMinutes: 300, offsetString: "+05:00", formatted: "UTC+05:00" }
|
|
129
|
+
*/
|
|
130
|
+
export function getTimezoneOffset(timezone = getUserTimezone(), date = new Date()) {
|
|
131
|
+
const d = parseDate(date);
|
|
132
|
+
try {
|
|
133
|
+
const parts = getTimezoneParts(d, timezone);
|
|
134
|
+
const utcParts = getTimezoneParts(d, 'UTC');
|
|
135
|
+
|
|
136
|
+
const tzDate = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
137
|
+
const utcDate = Date.UTC(utcParts.year, utcParts.month - 1, utcParts.day, utcParts.hour, utcParts.minute, utcParts.second);
|
|
138
|
+
|
|
139
|
+
const diffMinutes = Math.round((tzDate - utcDate) / (1000 * 60));
|
|
140
|
+
const sign = diffMinutes >= 0 ? '+' : '-';
|
|
141
|
+
const absMin = Math.abs(diffMinutes);
|
|
142
|
+
const hours = String(Math.floor(absMin / 60)).padStart(2, '0');
|
|
143
|
+
const mins = String(absMin % 60).padStart(2, '0');
|
|
144
|
+
const offsetString = `${sign}${hours}:${mins}`;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
offsetMinutes: diffMinutes,
|
|
148
|
+
offsetString,
|
|
149
|
+
formatted: `UTC${offsetString}`,
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return { offsetMinutes: 0, offsetString: '+00:00', formatted: 'UTC+00:00' };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── 3. DATE & TIME FORMATTING (WITH TOKENS) ────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const MONTH_NAMES = {
|
|
159
|
+
en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
160
|
+
ru: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const MONTH_SHORT = {
|
|
164
|
+
en: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
|
165
|
+
ru: ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format date in a specified timezone using custom format pattern tokens.
|
|
170
|
+
*
|
|
171
|
+
* Supported tokens:
|
|
172
|
+
* - YYYY / YY : Full / 2-digit year
|
|
173
|
+
* - MMMM / MMM / MM / M : Full / short / padded / unpadded month
|
|
174
|
+
* - DD / D : 2-digit / unpadded day
|
|
175
|
+
* - HH / H : 24-hour padded / unpadded
|
|
176
|
+
* - hh / h : 12-hour padded / unpadded
|
|
177
|
+
* - mm / m : Minutes padded / unpadded
|
|
178
|
+
* - ss / s : Seconds padded / unpadded
|
|
179
|
+
* - SSS : Milliseconds
|
|
180
|
+
* - A / a : AM/PM / am/pm
|
|
181
|
+
* - ZZ / Z : +05:00 / +0500
|
|
182
|
+
* - z : Timezone offset (e.g. UTC+05:00)
|
|
183
|
+
*
|
|
184
|
+
* @param {Date|number|string} date
|
|
185
|
+
* @param {string} [pattern='YYYY-MM-DD HH:mm:ss']
|
|
186
|
+
* @param {object} [options]
|
|
187
|
+
* @param {string} [options.timezone] Target timezone (default: user timezone)
|
|
188
|
+
* @param {string} [options.locale='en'] Locale for month/day names ('en' | 'ru')
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
export function formatTZ(date, pattern = 'YYYY-MM-DD HH:mm:ss', { timezone = getUserTimezone(), locale = 'en' } = {}) {
|
|
192
|
+
const d = parseDate(date);
|
|
193
|
+
const p = getTimezoneParts(d, timezone);
|
|
194
|
+
const offset = getTimezoneOffset(timezone, d);
|
|
195
|
+
|
|
196
|
+
const lang = MONTH_NAMES[locale] ? locale : 'en';
|
|
197
|
+
const monthIdx = p.month - 1;
|
|
198
|
+
const hour12 = p.hour % 12 || 12;
|
|
199
|
+
const isPM = p.hour >= 12;
|
|
200
|
+
|
|
201
|
+
const tokens = {
|
|
202
|
+
YYYY: String(p.year),
|
|
203
|
+
YY: String(p.year).slice(-2),
|
|
204
|
+
MMMM: MONTH_NAMES[lang][monthIdx],
|
|
205
|
+
MMM: MONTH_SHORT[lang][monthIdx],
|
|
206
|
+
MM: String(p.month).padStart(2, '0'),
|
|
207
|
+
M: String(p.month),
|
|
208
|
+
DD: String(p.day).padStart(2, '0'),
|
|
209
|
+
D: String(p.day),
|
|
210
|
+
HH: String(p.hour).padStart(2, '0'),
|
|
211
|
+
H: String(p.hour),
|
|
212
|
+
hh: String(hour12).padStart(2, '0'),
|
|
213
|
+
h: String(hour12),
|
|
214
|
+
mm: String(p.minute).padStart(2, '0'),
|
|
215
|
+
m: String(p.minute),
|
|
216
|
+
ss: String(p.second).padStart(2, '0'),
|
|
217
|
+
s: String(p.second),
|
|
218
|
+
SSS: String(p.millisecond).padStart(3, '0'),
|
|
219
|
+
A: isPM ? 'PM' : 'AM',
|
|
220
|
+
a: isPM ? 'pm' : 'am',
|
|
221
|
+
ZZ: offset.offsetString,
|
|
222
|
+
Z: offset.offsetString.replace(':', ''),
|
|
223
|
+
z: offset.formatted,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return pattern.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|SSS|A|a|ZZ|Z|z/g, (match) => tokens[match] || match);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── 4. RELATIVE TIME («TIME AGO») ──────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Format relative time (e.g. "5 minutes ago", "just now", "in 2 days").
|
|
233
|
+
*
|
|
234
|
+
* @param {Date|number|string} date Target date
|
|
235
|
+
* @param {object} [options]
|
|
236
|
+
* @param {Date|number|string} [options.baseDate=Date.now()] Base comparison date
|
|
237
|
+
* @param {string} [options.locale='en'] Locale (e.g. 'en', 'ru')
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
export function timeAgo(date, { baseDate = Date.now(), locale = 'en' } = {}) {
|
|
241
|
+
const d = parseDate(date).getTime();
|
|
242
|
+
const base = parseDate(baseDate).getTime();
|
|
243
|
+
const diffSec = Math.round((d - base) / 1000);
|
|
244
|
+
|
|
245
|
+
const rtf = typeof Intl !== 'undefined' && Intl.RelativeTimeFormat
|
|
246
|
+
? new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
|
|
247
|
+
: null;
|
|
248
|
+
|
|
249
|
+
const absSec = Math.abs(diffSec);
|
|
250
|
+
|
|
251
|
+
if (absSec < 10) {
|
|
252
|
+
return locale.startsWith('ru') ? 'только что' : 'just now';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const units = [
|
|
256
|
+
{ name: 'year', seconds: 31536000 },
|
|
257
|
+
{ name: 'month', seconds: 2592000 },
|
|
258
|
+
{ name: 'day', seconds: 86400 },
|
|
259
|
+
{ name: 'hour', seconds: 3600 },
|
|
260
|
+
{ name: 'minute', seconds: 60 },
|
|
261
|
+
{ name: 'second', seconds: 1 },
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
for (const { name, seconds } of units) {
|
|
265
|
+
if (absSec >= seconds || name === 'second') {
|
|
266
|
+
const val = Math.round(diffSec / seconds);
|
|
267
|
+
if (rtf) {
|
|
268
|
+
return rtf.format(val, name);
|
|
269
|
+
}
|
|
270
|
+
return `${Math.abs(val)} ${name}${Math.abs(val) > 1 ? 's' : ''} ${diffSec < 0 ? 'ago' : 'from now'}`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── 5. TIMEZONE-AWARE CURRENT TIME & STRING DATES ──────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns a Date object shifted to the wall-clock time of the target timezone.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} [timezone] Target IANA timezone (default: user timezone)
|
|
283
|
+
* @returns {Date}
|
|
284
|
+
*/
|
|
285
|
+
export function getNow(timezone = getUserTimezone()) {
|
|
286
|
+
const d = new Date();
|
|
287
|
+
const offset = getTimezoneOffset(timezone, d);
|
|
288
|
+
return new Date(d.getTime() + offset.offsetMinutes * 60 * 1000);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get current date string in 'YYYY-MM-DD' format for a given timezone.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} [timezone] Target IANA timezone (default: user timezone)
|
|
295
|
+
* @returns {string} E.g. "2026-09-08"
|
|
296
|
+
*/
|
|
297
|
+
export function getToday(timezone = getUserTimezone()) {
|
|
298
|
+
const p = getTimezoneParts(new Date(), timezone);
|
|
299
|
+
return `${p.year}-${pad(p.month)}-${pad(p.day)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get current month string in 'YYYY-MM' format for a given timezone.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} [timezone] Target IANA timezone (default: user timezone)
|
|
306
|
+
* @returns {string} E.g. "2026-09"
|
|
307
|
+
*/
|
|
308
|
+
export function getCurrentMonth(timezone = getUserTimezone()) {
|
|
309
|
+
const p = getTimezoneParts(new Date(), timezone);
|
|
310
|
+
return `${p.year}-${pad(p.month)}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── 6. STRING-BASED DATE ARITHMETIC & RANGES ───────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Shift a date string ('YYYY-MM-DD' or ISO) by an amount of days, months, or years.
|
|
317
|
+
* Leap-year safe and timezone-independent.
|
|
318
|
+
*
|
|
319
|
+
* @param {string|Date} dateInput
|
|
320
|
+
* @param {number} [amount=0] Amount to shift (negative for past, positive for future)
|
|
321
|
+
* @param {'days'|'months'|'years'} [unit='days']
|
|
322
|
+
* @returns {string} Date string in 'YYYY-MM-DD' format
|
|
323
|
+
*/
|
|
324
|
+
export function shiftDateStr(dateInput, amount = 0, unit = 'days') {
|
|
325
|
+
let y, m, d;
|
|
326
|
+
if (typeof dateInput === 'string' && /^\d{4}-\d{2}-\d{2}/.test(dateInput)) {
|
|
327
|
+
const parts = dateInput.slice(0, 10).split('-').map(Number);
|
|
328
|
+
y = parts[0];
|
|
329
|
+
m = parts[1];
|
|
330
|
+
d = parts[2];
|
|
331
|
+
} else {
|
|
332
|
+
const parsed = parseDate(dateInput);
|
|
333
|
+
y = parsed.getUTCFullYear();
|
|
334
|
+
m = parsed.getUTCMonth() + 1;
|
|
335
|
+
d = parsed.getUTCDate();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
339
|
+
switch (unit) {
|
|
340
|
+
case 'years':
|
|
341
|
+
dt.setUTCFullYear(dt.getUTCFullYear() + amount);
|
|
342
|
+
break;
|
|
343
|
+
case 'months':
|
|
344
|
+
dt.setUTCMonth(dt.getUTCMonth() + amount);
|
|
345
|
+
break;
|
|
346
|
+
case 'days':
|
|
347
|
+
default:
|
|
348
|
+
dt.setUTCDate(dt.getUTCDate() + amount);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return `${dt.getUTCFullYear()}-${pad(dt.getUTCMonth() + 1)}-${pad(dt.getUTCDate())}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Shift a date string by N days into the past.
|
|
357
|
+
*
|
|
358
|
+
* @param {string|Date} dateStr
|
|
359
|
+
* @param {number} [count=1] Number of days to go back
|
|
360
|
+
* @returns {string} Date string in 'YYYY-MM-DD' format
|
|
361
|
+
*/
|
|
362
|
+
export function prevDayStr(dateStr, count = 1) {
|
|
363
|
+
return shiftDateStr(dateStr, -count, 'days');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Shift a date string by N days into the future.
|
|
368
|
+
*
|
|
369
|
+
* @param {string|Date} dateStr
|
|
370
|
+
* @param {number} [count=1] Number of days to advance
|
|
371
|
+
* @returns {string} Date string in 'YYYY-MM-DD' format
|
|
372
|
+
*/
|
|
373
|
+
export function nextDayStr(dateStr, count = 1) {
|
|
374
|
+
return shiftDateStr(dateStr, count, 'days');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Calculate the number of calendar days between two dates (date2 - date1).
|
|
379
|
+
*
|
|
380
|
+
* @param {string|Date} date1
|
|
381
|
+
* @param {string|Date} date2
|
|
382
|
+
* @returns {number} Difference in days (positive if date2 is after date1)
|
|
383
|
+
*/
|
|
384
|
+
export function daysBetween(date1, date2) {
|
|
385
|
+
const parseToUtcMidnight = (val) => {
|
|
386
|
+
if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val)) {
|
|
387
|
+
const [y, m, d] = val.slice(0, 10).split('-').map(Number);
|
|
388
|
+
return Date.UTC(y, m - 1, d);
|
|
389
|
+
}
|
|
390
|
+
const dt = parseDate(val);
|
|
391
|
+
return Date.UTC(dt.getUTCFullYear(), dt.getUTCMonth(), dt.getUTCDate());
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const ms1 = parseToUtcMidnight(date1);
|
|
395
|
+
const ms2 = parseToUtcMidnight(date2);
|
|
396
|
+
return Math.round((ms2 - ms1) / (1000 * 60 * 60 * 24));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Generate an array of date strings ('YYYY-MM-DD') for every day in the range [startDate, endDate].
|
|
401
|
+
*
|
|
402
|
+
* @param {string|Date} startDate
|
|
403
|
+
* @param {string|Date} endDate
|
|
404
|
+
* @returns {string[]} Array of date strings
|
|
405
|
+
*/
|
|
406
|
+
export function getDateRange(startDate, endDate) {
|
|
407
|
+
const startStr = typeof startDate === 'string' && /^\d{4}-\d{2}-\d{2}/.test(startDate)
|
|
408
|
+
? startDate.slice(0, 10)
|
|
409
|
+
: formatTZ(startDate, 'YYYY-MM-DD');
|
|
410
|
+
const endStr = typeof endDate === 'string' && /^\d{4}-\d{2}-\d{2}/.test(endDate)
|
|
411
|
+
? endDate.slice(0, 10)
|
|
412
|
+
: formatTZ(endDate, 'YYYY-MM-DD');
|
|
413
|
+
|
|
414
|
+
if (startStr > endStr) return [];
|
|
415
|
+
|
|
416
|
+
const range = [];
|
|
417
|
+
let curr = startStr;
|
|
418
|
+
while (curr <= endStr) {
|
|
419
|
+
range.push(curr);
|
|
420
|
+
curr = nextDayStr(curr);
|
|
421
|
+
}
|
|
422
|
+
return range;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get start of month date string ('YYYY-MM-01').
|
|
427
|
+
*
|
|
428
|
+
* @param {string|Date} [date=new Date()]
|
|
429
|
+
* @param {string} [timezone]
|
|
430
|
+
* @returns {string} E.g. "2026-09-01"
|
|
431
|
+
*/
|
|
432
|
+
export function getStartOfMonth(date = new Date(), timezone = getUserTimezone()) {
|
|
433
|
+
const p = getTimezoneParts(date, timezone);
|
|
434
|
+
return `${p.year}-${pad(p.month)}-01`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get end of month date string (e.g. '2026-02-28', '2026-09-30').
|
|
439
|
+
*
|
|
440
|
+
* @param {string|Date} [date=new Date()]
|
|
441
|
+
* @param {string} [timezone]
|
|
442
|
+
* @returns {string}
|
|
443
|
+
*/
|
|
444
|
+
export function getEndOfMonth(date = new Date(), timezone = getUserTimezone()) {
|
|
445
|
+
const p = getTimezoneParts(date, timezone);
|
|
446
|
+
const dt = new Date(Date.UTC(p.year, p.month, 0));
|
|
447
|
+
return `${p.year}-${pad(p.month)}-${pad(dt.getUTCDate())}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get exact Start of Day (00:00:00.000) as a Date object for a given date and timezone.
|
|
452
|
+
*
|
|
453
|
+
* @param {string|Date} [date=new Date()]
|
|
454
|
+
* @param {string} [timezone]
|
|
455
|
+
* @returns {Date}
|
|
456
|
+
*/
|
|
457
|
+
export function getStartOfDay(date = new Date(), timezone = getUserTimezone()) {
|
|
458
|
+
const p = getTimezoneParts(date, timezone);
|
|
459
|
+
const offset = getTimezoneOffset(timezone, new Date(Date.UTC(p.year, p.month - 1, p.day, 12, 0, 0)));
|
|
460
|
+
return new Date(Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0, 0) - offset.offsetMinutes * 60 * 1000);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get exact End of Day (23:59:59.999) as a Date object for a given date and timezone.
|
|
465
|
+
*
|
|
466
|
+
* @param {string|Date} [date=new Date()]
|
|
467
|
+
* @param {string} [timezone]
|
|
468
|
+
* @returns {Date}
|
|
469
|
+
*/
|
|
470
|
+
export function getEndOfDay(date = new Date(), timezone = getUserTimezone()) {
|
|
471
|
+
const p = getTimezoneParts(date, timezone);
|
|
472
|
+
const offset = getTimezoneOffset(timezone, new Date(Date.UTC(p.year, p.month - 1, p.day, 12, 0, 0)));
|
|
473
|
+
return new Date(Date.UTC(p.year, p.month - 1, p.day, 23, 59, 59, 999) - offset.offsetMinutes * 60 * 1000);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ─── 7. DATE ARITHMETIC & COMPARISONS ───────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Add or subtract time from a Date object.
|
|
480
|
+
*
|
|
481
|
+
* @param {Date|number|string} date
|
|
482
|
+
* @param {number} amount Amount to add (negative to subtract)
|
|
483
|
+
* @param {'years'|'months'|'days'|'hours'|'minutes'|'seconds'} unit
|
|
484
|
+
* @returns {Date}
|
|
485
|
+
*/
|
|
486
|
+
export function addTime(date, amount, unit = 'days') {
|
|
487
|
+
const d = parseDate(date);
|
|
488
|
+
switch (unit) {
|
|
489
|
+
case 'years':
|
|
490
|
+
d.setFullYear(d.getFullYear() + amount);
|
|
491
|
+
break;
|
|
492
|
+
case 'months':
|
|
493
|
+
d.setMonth(d.getMonth() + amount);
|
|
494
|
+
break;
|
|
495
|
+
case 'days':
|
|
496
|
+
d.setDate(d.getDate() + amount);
|
|
497
|
+
break;
|
|
498
|
+
case 'hours':
|
|
499
|
+
d.setHours(d.getHours() + amount);
|
|
500
|
+
break;
|
|
501
|
+
case 'minutes':
|
|
502
|
+
d.setMinutes(d.getMinutes() + amount);
|
|
503
|
+
break;
|
|
504
|
+
case 'seconds':
|
|
505
|
+
d.setSeconds(d.getSeconds() + amount);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
return d;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Check if two dates fall on the same day in a given timezone.
|
|
513
|
+
*/
|
|
514
|
+
export function isSameDay(date1, date2, timezone = getUserTimezone()) {
|
|
515
|
+
const p1 = getTimezoneParts(date1, timezone);
|
|
516
|
+
const p2 = getTimezoneParts(date2, timezone);
|
|
517
|
+
return p1.year === p2.year && p1.month === p2.month && p1.day === p2.day;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Check if a date is today in a given timezone.
|
|
522
|
+
*/
|
|
523
|
+
export function isToday(date, timezone = getUserTimezone()) {
|
|
524
|
+
return isSameDay(date, new Date(), timezone);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ─── 8. TIMEZONE LIST GENERATOR (FOR SELECT/DROPDOWNS) ──────────────────────────
|
|
528
|
+
|
|
529
|
+
export const COMMON_TIMEZONES = [
|
|
530
|
+
{ value: 'Pacific/Midway', label: '(UTC-11:00) Midway Island, Samoa' },
|
|
531
|
+
{ value: 'Pacific/Honolulu', label: '(UTC-10:00) Hawaii' },
|
|
532
|
+
{ value: 'America/Anchorage', label: '(UTC-09:00) Alaska' },
|
|
533
|
+
{ value: 'America/Los_Angeles', label: '(UTC-08:00) Pacific Time (US & Canada)' },
|
|
534
|
+
{ value: 'America/Denver', label: '(UTC-07:00) Mountain Time (US & Canada)' },
|
|
535
|
+
{ value: 'America/Chicago', label: '(UTC-06:00) Central Time (US & Canada)' },
|
|
536
|
+
{ value: 'America/New_York', label: '(UTC-05:00) Eastern Time (US & Canada)' },
|
|
537
|
+
{ value: 'America/Sao_Paulo', label: '(UTC-03:00) Brasilia, Sao Paulo' },
|
|
538
|
+
{ value: 'UTC', label: '(UTC+00:00) Universal Coordinated Time, London' },
|
|
539
|
+
{ value: 'Europe/London', label: '(UTC+00:00) London, Dublin, Edinburgh' },
|
|
540
|
+
{ value: 'Europe/Paris', label: '(UTC+01:00) Paris, Berlin, Rome, Madrid' },
|
|
541
|
+
{ value: 'Europe/Kyiv', label: '(UTC+02:00) Kyiv, Helsinki, Athens' },
|
|
542
|
+
{ value: 'Europe/Moscow', label: '(UTC+03:00) Moscow, St. Petersburg, Istanbul' },
|
|
543
|
+
{ value: 'Asia/Dubai', label: '(UTC+04:00) Dubai, Abu Dhabi, Baku' },
|
|
544
|
+
{ value: 'Asia/Tashkent', label: '(UTC+05:00) Tashkent, Samarkand, Islamabad' },
|
|
545
|
+
{ value: 'Asia/Almaty', label: '(UTC+05:00) Almaty, Astana' },
|
|
546
|
+
{ value: 'Asia/Dhaka', label: '(UTC+06:00) Dhaka, Novosibirsk' },
|
|
547
|
+
{ value: 'Asia/Bangkok', label: '(UTC+07:00) Bangkok, Hanoi, Jakarta' },
|
|
548
|
+
{ value: 'Asia/Singapore', label: '(UTC+08:00) Singapore, Beijing, Hong Kong' },
|
|
549
|
+
{ value: 'Asia/Tokyo', label: '(UTC+09:00) Tokyo, Seoul' },
|
|
550
|
+
{ value: 'Australia/Sydney', label: '(UTC+10:00) Sydney, Melbourne' },
|
|
551
|
+
{ value: 'Pacific/Auckland', label: '(UTC+12:00) Auckland, Wellington' },
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Return a list of major timezones with their current real-time UTC offsets.
|
|
556
|
+
* Ideal for populating Select/Dropdown components.
|
|
557
|
+
*
|
|
558
|
+
* @returns {Array<{ value: string, label: string, offset: string, offsetMinutes: number }>}
|
|
559
|
+
*/
|
|
560
|
+
export function getTimezoneList() {
|
|
561
|
+
return COMMON_TIMEZONES.map((tz) => {
|
|
562
|
+
const off = getTimezoneOffset(tz.value);
|
|
563
|
+
return {
|
|
564
|
+
value: tz.value,
|
|
565
|
+
label: tz.label,
|
|
566
|
+
offset: off.offsetString,
|
|
567
|
+
offsetMinutes: off.offsetMinutes,
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ─── 9. BACKWARD COMPATIBILITY (TASHKENT SHORTCUTS) ─────────────────────────────
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* @deprecated Use getNow('Asia/Tashkent')
|
|
576
|
+
*/
|
|
577
|
+
export function tashkentNow() {
|
|
578
|
+
return getNow('Asia/Tashkent');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @deprecated Use getToday('Asia/Tashkent')
|
|
583
|
+
*/
|
|
584
|
+
export function tashkentToday() {
|
|
585
|
+
return getToday('Asia/Tashkent');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @deprecated Use getCurrentMonth('Asia/Tashkent')
|
|
590
|
+
*/
|
|
591
|
+
export function tashkentMonth() {
|
|
592
|
+
return getCurrentMonth('Asia/Tashkent');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
export default {
|
|
598
|
+
pad,
|
|
599
|
+
getUserTimezone,
|
|
600
|
+
isValidTimezone,
|
|
601
|
+
getTimezoneParts,
|
|
602
|
+
getTimezoneOffset,
|
|
603
|
+
formatTZ,
|
|
604
|
+
timeAgo,
|
|
605
|
+
getNow,
|
|
606
|
+
getToday,
|
|
607
|
+
getCurrentMonth,
|
|
608
|
+
shiftDateStr,
|
|
609
|
+
prevDayStr,
|
|
610
|
+
nextDayStr,
|
|
611
|
+
daysBetween,
|
|
612
|
+
getDateRange,
|
|
613
|
+
getStartOfMonth,
|
|
614
|
+
getEndOfMonth,
|
|
615
|
+
getStartOfDay,
|
|
616
|
+
getEndOfDay,
|
|
617
|
+
addTime,
|
|
618
|
+
isSameDay,
|
|
619
|
+
isToday,
|
|
620
|
+
getTimezoneList,
|
|
621
|
+
COMMON_TIMEZONES,
|
|
622
|
+
tashkentNow,
|
|
623
|
+
tashkentToday,
|
|
624
|
+
tashkentMonth,
|
|
625
|
+
};
|