@getmicdrop/venue-calendar 3.3.0 → 3.3.1
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/README.md +661 -661
- package/dist/{VenueCalendar-BMSfRl2d.js → VenueCalendar-Xppig0q_.js} +13 -10
- package/dist/VenueCalendar-Xppig0q_.js.map +1 -0
- package/dist/{index-CoJaem3n.js → index-BjErG0CG.js} +2 -2
- package/dist/{index-CoJaem3n.js.map → index-BjErG0CG.js.map} +1 -1
- package/dist/types/index.d.ts +395 -395
- package/dist/venue-calendar.css +1 -1
- package/dist/venue-calendar.es.js +1 -1
- package/dist/venue-calendar.iife.js +4 -4
- package/dist/venue-calendar.iife.js.map +1 -1
- package/dist/venue-calendar.umd.js +4 -4
- package/dist/venue-calendar.umd.js.map +1 -1
- package/package.json +2 -1
- package/src/lib/api/client.ts +210 -210
- package/src/lib/api/events.ts +358 -358
- package/src/lib/api/index.ts +182 -182
- package/src/lib/api/orders.ts +390 -390
- package/src/lib/api/promo.ts +164 -164
- package/src/lib/api/transformers/event.ts +248 -248
- package/src/lib/api/transformers/index.ts +29 -29
- package/src/lib/api/transformers/order.ts +207 -207
- package/src/lib/api/transformers/venue.ts +118 -118
- package/src/lib/api/types.ts +289 -289
- package/src/lib/api/venues.ts +100 -100
- package/src/lib/theme.js +209 -209
- package/src/lib/utils/api.js +790 -0
- package/src/lib/utils/api.test.js +1284 -0
- package/src/lib/utils/constants.js +8 -0
- package/src/lib/utils/constants.test.js +39 -0
- package/src/lib/utils/datetime.js +266 -0
- package/src/lib/utils/datetime.test.js +340 -0
- package/src/lib/utils/event-transform.js +464 -0
- package/src/lib/utils/event-transform.test.js +413 -0
- package/src/lib/utils/logger.js +105 -0
- package/src/lib/utils/timezone.js +109 -0
- package/src/lib/utils/timezone.test.js +222 -0
- package/src/lib/utils/utils.js +806 -0
- package/src/lib/utils/utils.test.js +959 -0
- package/dist/VenueCalendar-BMSfRl2d.js.map +0 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the venue calendar components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Placeholder image for events without images
|
|
6
|
+
// Neutral gray background with a calendar icon
|
|
7
|
+
// SVG file located at /static/placeholder-event.svg
|
|
8
|
+
export const PLACEHOLDER_IMAGE = "/placeholder-event.svg";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PLACEHOLDER_IMAGE } from './constants.js';
|
|
3
|
+
|
|
4
|
+
describe('lib/utils/constants.js', () => {
|
|
5
|
+
describe('PLACEHOLDER_IMAGE', () => {
|
|
6
|
+
it('is exported', () => {
|
|
7
|
+
expect(PLACEHOLDER_IMAGE).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('is a string', () => {
|
|
11
|
+
expect(typeof PLACEHOLDER_IMAGE).toBe('string');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('is a path to placeholder-event.svg', () => {
|
|
15
|
+
expect(PLACEHOLDER_IMAGE).toBe('/placeholder-event.svg');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('starts with forward slash', () => {
|
|
19
|
+
expect(PLACEHOLDER_IMAGE.startsWith('/')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('ends with .svg extension', () => {
|
|
23
|
+
expect(PLACEHOLDER_IMAGE.endsWith('.svg')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('contains placeholder in the name', () => {
|
|
27
|
+
expect(PLACEHOLDER_IMAGE).toContain('placeholder');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('contains event in the name', () => {
|
|
31
|
+
expect(PLACEHOLDER_IMAGE).toContain('event');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('is a valid static asset path format', () => {
|
|
35
|
+
// Should be an absolute path starting with /
|
|
36
|
+
expect(PLACEHOLDER_IMAGE).toMatch(/^\/[\w-]+\.\w+$/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateTime Utilities
|
|
3
|
+
*
|
|
4
|
+
* Consolidated date/time formatting with timezone support.
|
|
5
|
+
* Uses Intl.DateTimeFormat for consistent timezone-aware formatting.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: When @getmicdrop/svelte-components is updated to include the datetime
|
|
8
|
+
* module, these functions can be replaced with re-exports.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { formatEventTime, formatTimeRange, getDateParts } from './datetime.js';
|
|
12
|
+
*
|
|
13
|
+
* const time = formatEventTime(event.startDateTime, event.timeZone);
|
|
14
|
+
* const range = formatTimeRange(event.startDateTime, event.endDateTime, event.timeZone);
|
|
15
|
+
* const { day, month, date } = getDateParts(event.startDateTime, event.timeZone);
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Re-export timezone utilities from existing module
|
|
19
|
+
export { getIANATimezone, LEGACY_TIMEZONE_MAP } from './timezone.js';
|
|
20
|
+
|
|
21
|
+
/** Default timezone when none specified */
|
|
22
|
+
export const DEFAULT_TIMEZONE = 'UTC';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Timezone Utilities
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a timezone string is valid IANA timezone.
|
|
30
|
+
* @param {string} tz - Timezone string to validate
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isValidTimezone(tz) {
|
|
34
|
+
if (!tz || typeof tz !== 'string') return false;
|
|
35
|
+
try {
|
|
36
|
+
Intl.DateTimeFormat(undefined, { timeZone: tz });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const isValidIANATimezone = isValidTimezone;
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Format Functions
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse and validate an ISO date string.
|
|
51
|
+
* @param {string} utcIso - ISO date string
|
|
52
|
+
* @returns {Date|null}
|
|
53
|
+
*/
|
|
54
|
+
function parseISO(utcIso) {
|
|
55
|
+
if (!utcIso || typeof utcIso !== 'string') return null;
|
|
56
|
+
const date = new Date(utcIso);
|
|
57
|
+
return isNaN(date.getTime()) ? null : date;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format a datetime in the specified timezone.
|
|
62
|
+
* @param {string} utcIso - ISO date string
|
|
63
|
+
* @param {string} timezone - IANA timezone
|
|
64
|
+
* @param {Object} options - Intl.DateTimeFormat options
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function formatInTz(utcIso, timezone, options) {
|
|
68
|
+
const date = parseISO(utcIso);
|
|
69
|
+
if (!date) return '';
|
|
70
|
+
|
|
71
|
+
const tz = isValidTimezone(timezone) ? timezone : DEFAULT_TIMEZONE;
|
|
72
|
+
return new Intl.DateTimeFormat('en-US', { ...options, timeZone: tz }).format(date);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Format event time (e.g., "7:00 PM").
|
|
77
|
+
* @param {string} utcIso - ISO date string
|
|
78
|
+
* @param {string} timezone - IANA timezone
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function formatEventTime(utcIso, timezone) {
|
|
82
|
+
return formatInTz(utcIso, timezone, {
|
|
83
|
+
hour: 'numeric',
|
|
84
|
+
minute: '2-digit',
|
|
85
|
+
hour12: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format event date (e.g., "Dec 25, 2023").
|
|
91
|
+
* @param {string} utcIso - ISO date string
|
|
92
|
+
* @param {string} timezone - IANA timezone
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function formatEventDate(utcIso, timezone) {
|
|
96
|
+
return formatInTz(utcIso, timezone, {
|
|
97
|
+
month: 'short',
|
|
98
|
+
day: 'numeric',
|
|
99
|
+
year: 'numeric',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Format event datetime (e.g., "Dec 25, 2023 at 7:00 PM").
|
|
105
|
+
* @param {string} utcIso - ISO date string
|
|
106
|
+
* @param {string} timezone - IANA timezone
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
export function formatEventDateTime(utcIso, timezone) {
|
|
110
|
+
const date = formatEventDate(utcIso, timezone);
|
|
111
|
+
const time = formatEventTime(utcIso, timezone);
|
|
112
|
+
return date && time ? `${date} at ${time}` : date || time;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Format time range (e.g., "7:00 PM - 10:00 PM").
|
|
117
|
+
* @param {string} startUtc - Start ISO date string
|
|
118
|
+
* @param {string} endUtc - End ISO date string
|
|
119
|
+
* @param {string} timezone - IANA timezone
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function formatTimeRange(startUtc, endUtc, timezone) {
|
|
123
|
+
const start = formatEventTime(startUtc, timezone);
|
|
124
|
+
if (!endUtc) return start;
|
|
125
|
+
|
|
126
|
+
const end = formatEventTime(endUtc, timezone);
|
|
127
|
+
return start && end ? `${start} - ${end}` : start;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format clean time range (omits :00 minutes).
|
|
132
|
+
* @param {string} startUtc - Start ISO date string
|
|
133
|
+
* @param {string} endUtc - End ISO date string
|
|
134
|
+
* @param {string} timezone - IANA timezone
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
export function formatCleanTimeRange(startUtc, endUtc, timezone) {
|
|
138
|
+
const formatClean = (utcIso) => {
|
|
139
|
+
const date = parseISO(utcIso);
|
|
140
|
+
if (!date) return '';
|
|
141
|
+
|
|
142
|
+
const tz = isValidTimezone(timezone) ? timezone : DEFAULT_TIMEZONE;
|
|
143
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
144
|
+
hour: 'numeric',
|
|
145
|
+
minute: '2-digit',
|
|
146
|
+
hour12: true,
|
|
147
|
+
timeZone: tz,
|
|
148
|
+
}).formatToParts(date);
|
|
149
|
+
|
|
150
|
+
const hour = parts.find(p => p.type === 'hour')?.value || '';
|
|
151
|
+
const minute = parts.find(p => p.type === 'minute')?.value || '00';
|
|
152
|
+
const period = parts.find(p => p.type === 'dayPeriod')?.value || '';
|
|
153
|
+
|
|
154
|
+
return minute === '00' ? `${hour} ${period}` : `${hour}:${minute} ${period}`;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const start = formatClean(startUtc);
|
|
158
|
+
if (!endUtc) return start;
|
|
159
|
+
|
|
160
|
+
const end = formatClean(endUtc);
|
|
161
|
+
return start && end ? `${start} - ${end}` : start;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format day of week (e.g., "Mon" or "Monday").
|
|
166
|
+
* @param {string} utcIso - ISO date string
|
|
167
|
+
* @param {string} timezone - IANA timezone
|
|
168
|
+
* @param {boolean} short - Use short format (default: true)
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
export function formatDayOfWeek(utcIso, timezone, short = true) {
|
|
172
|
+
return formatInTz(utcIso, timezone, {
|
|
173
|
+
weekday: short ? 'short' : 'long',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format month (e.g., "Dec" or "December").
|
|
179
|
+
* @param {string} utcIso - ISO date string
|
|
180
|
+
* @param {string} timezone - IANA timezone
|
|
181
|
+
* @param {boolean} short - Use short format (default: true)
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
export function formatMonth(utcIso, timezone, short = true) {
|
|
185
|
+
return formatInTz(utcIso, timezone, {
|
|
186
|
+
month: short ? 'short' : 'long',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Format hour (e.g., "8 PM").
|
|
192
|
+
* @param {number} hour - Hour (0-23)
|
|
193
|
+
* @returns {string}
|
|
194
|
+
*/
|
|
195
|
+
export function formatHour(hour) {
|
|
196
|
+
const suffix = hour >= 12 ? 'PM' : 'AM';
|
|
197
|
+
const displayHour = hour % 12 || 12;
|
|
198
|
+
return `${displayHour} ${suffix}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get date parts for display.
|
|
203
|
+
* @param {string} utcIso - ISO date string
|
|
204
|
+
* @param {string} timezone - IANA timezone
|
|
205
|
+
* @returns {{ day: string, month: string, date: number, year: number }}
|
|
206
|
+
*/
|
|
207
|
+
export function getDateParts(utcIso, timezone) {
|
|
208
|
+
const date = parseISO(utcIso);
|
|
209
|
+
if (!date) return { day: '', month: '', date: 0, year: 0 };
|
|
210
|
+
|
|
211
|
+
const tz = isValidTimezone(timezone) ? timezone : DEFAULT_TIMEZONE;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
day: formatInTz(utcIso, tz, { weekday: 'short' }),
|
|
215
|
+
month: formatInTz(utcIso, tz, { month: 'short' }),
|
|
216
|
+
date: parseInt(formatInTz(utcIso, tz, { day: 'numeric' }), 10),
|
|
217
|
+
year: parseInt(formatInTz(utcIso, tz, { year: 'numeric' }), 10),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if a date is today in the specified timezone.
|
|
223
|
+
* @param {string} utcIso - ISO date string
|
|
224
|
+
* @param {string} timezone - IANA timezone
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
export function isToday(utcIso, timezone) {
|
|
228
|
+
const date = parseISO(utcIso);
|
|
229
|
+
if (!date) return false;
|
|
230
|
+
|
|
231
|
+
const tz = isValidTimezone(timezone) ? timezone : DEFAULT_TIMEZONE;
|
|
232
|
+
const now = new Date();
|
|
233
|
+
|
|
234
|
+
const dateStr = formatInTz(utcIso, tz, { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
235
|
+
const nowStr = new Intl.DateTimeFormat('en-US', {
|
|
236
|
+
year: 'numeric',
|
|
237
|
+
month: '2-digit',
|
|
238
|
+
day: '2-digit',
|
|
239
|
+
timeZone: tz,
|
|
240
|
+
}).format(now);
|
|
241
|
+
|
|
242
|
+
return dateStr === nowStr;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format notification time (relative for recent, date for older).
|
|
247
|
+
* @param {string} utcIso - ISO date string
|
|
248
|
+
* @param {string} timezone - IANA timezone
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
export function formatNotificationTime(utcIso, timezone) {
|
|
252
|
+
const date = parseISO(utcIso);
|
|
253
|
+
if (!date) return '';
|
|
254
|
+
|
|
255
|
+
const now = new Date();
|
|
256
|
+
const diffMs = now.getTime() - date.getTime();
|
|
257
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
258
|
+
|
|
259
|
+
if (isToday(utcIso, timezone)) {
|
|
260
|
+
if (diffSec < 60) return `${diffSec} seconds ago`;
|
|
261
|
+
if (diffSec < 3600) return `${Math.floor(diffSec / 60)} minutes ago`;
|
|
262
|
+
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)} hours ago`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return formatEventDateTime(utcIso, timezone);
|
|
266
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isValidTimezone,
|
|
4
|
+
isValidIANATimezone,
|
|
5
|
+
formatEventTime,
|
|
6
|
+
formatEventDate,
|
|
7
|
+
formatEventDateTime,
|
|
8
|
+
formatTimeRange,
|
|
9
|
+
formatCleanTimeRange,
|
|
10
|
+
formatDayOfWeek,
|
|
11
|
+
formatMonth,
|
|
12
|
+
formatHour,
|
|
13
|
+
getDateParts,
|
|
14
|
+
isToday,
|
|
15
|
+
formatNotificationTime,
|
|
16
|
+
DEFAULT_TIMEZONE,
|
|
17
|
+
} from './datetime.js';
|
|
18
|
+
|
|
19
|
+
describe('datetime.js', () => {
|
|
20
|
+
describe('isValidTimezone', () => {
|
|
21
|
+
it('returns true for valid IANA timezones', () => {
|
|
22
|
+
expect(isValidTimezone('America/New_York')).toBe(true);
|
|
23
|
+
expect(isValidTimezone('America/Los_Angeles')).toBe(true);
|
|
24
|
+
expect(isValidTimezone('Europe/London')).toBe(true);
|
|
25
|
+
expect(isValidTimezone('UTC')).toBe(true);
|
|
26
|
+
expect(isValidTimezone('Asia/Tokyo')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns false for invalid timezones', () => {
|
|
30
|
+
expect(isValidTimezone('Invalid/Timezone')).toBe(false);
|
|
31
|
+
expect(isValidTimezone('Eastern')).toBe(false); // Not valid
|
|
32
|
+
expect(isValidTimezone('PDT')).toBe(false); // Daylight time abbreviations are not valid
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns true for abbreviations that are valid in Node.js', () => {
|
|
36
|
+
// Some abbreviations are actually valid in Node.js Intl
|
|
37
|
+
expect(isValidTimezone('PST')).toBe(true);
|
|
38
|
+
expect(isValidTimezone('EST')).toBe(true);
|
|
39
|
+
expect(isValidTimezone('GMT')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns false for null/undefined/empty', () => {
|
|
43
|
+
expect(isValidTimezone(null)).toBe(false);
|
|
44
|
+
expect(isValidTimezone(undefined)).toBe(false);
|
|
45
|
+
expect(isValidTimezone('')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns false for non-string values', () => {
|
|
49
|
+
expect(isValidTimezone(123)).toBe(false);
|
|
50
|
+
expect(isValidTimezone({})).toBe(false);
|
|
51
|
+
expect(isValidTimezone([])).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isValidIANATimezone', () => {
|
|
56
|
+
it('is an alias for isValidTimezone', () => {
|
|
57
|
+
expect(isValidIANATimezone).toBe(isValidTimezone);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('formatEventTime', () => {
|
|
62
|
+
it('formats time correctly in UTC', () => {
|
|
63
|
+
const result = formatEventTime('2024-01-15T19:00:00Z', 'UTC');
|
|
64
|
+
expect(result).toBe('7:00 PM');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('formats time with minutes', () => {
|
|
68
|
+
const result = formatEventTime('2024-01-15T19:30:00Z', 'UTC');
|
|
69
|
+
expect(result).toBe('7:30 PM');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('formats morning time', () => {
|
|
73
|
+
const result = formatEventTime('2024-01-15T09:00:00Z', 'UTC');
|
|
74
|
+
expect(result).toBe('9:00 AM');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles timezone conversion', () => {
|
|
78
|
+
// 7 PM UTC should be 2 PM in New York (EST = UTC-5)
|
|
79
|
+
const result = formatEventTime('2024-01-15T19:00:00Z', 'America/New_York');
|
|
80
|
+
expect(result).toBe('2:00 PM');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns empty string for invalid date', () => {
|
|
84
|
+
expect(formatEventTime('invalid', 'UTC')).toBe('');
|
|
85
|
+
expect(formatEventTime(null, 'UTC')).toBe('');
|
|
86
|
+
expect(formatEventTime('', 'UTC')).toBe('');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('falls back to UTC for invalid timezone', () => {
|
|
90
|
+
const result = formatEventTime('2024-01-15T19:00:00Z', 'Invalid/Timezone');
|
|
91
|
+
expect(result).toBe('7:00 PM');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('formatEventDate', () => {
|
|
96
|
+
it('formats date correctly', () => {
|
|
97
|
+
const result = formatEventDate('2024-01-15T19:00:00Z', 'UTC');
|
|
98
|
+
expect(result).toBe('Jan 15, 2024');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles different months', () => {
|
|
102
|
+
expect(formatEventDate('2024-12-25T12:00:00Z', 'UTC')).toBe('Dec 25, 2024');
|
|
103
|
+
expect(formatEventDate('2024-06-01T12:00:00Z', 'UTC')).toBe('Jun 1, 2024');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns empty string for invalid date', () => {
|
|
107
|
+
expect(formatEventDate('invalid', 'UTC')).toBe('');
|
|
108
|
+
expect(formatEventDate(null, 'UTC')).toBe('');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('formatEventDateTime', () => {
|
|
113
|
+
it('formats date and time together', () => {
|
|
114
|
+
const result = formatEventDateTime('2024-01-15T19:00:00Z', 'UTC');
|
|
115
|
+
expect(result).toBe('Jan 15, 2024 at 7:00 PM');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns empty string for invalid date', () => {
|
|
119
|
+
expect(formatEventDateTime('invalid', 'UTC')).toBe('');
|
|
120
|
+
expect(formatEventDateTime(null, 'UTC')).toBe('');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('formatTimeRange', () => {
|
|
125
|
+
it('formats start and end time', () => {
|
|
126
|
+
const result = formatTimeRange(
|
|
127
|
+
'2024-01-15T19:00:00Z',
|
|
128
|
+
'2024-01-15T22:00:00Z',
|
|
129
|
+
'UTC'
|
|
130
|
+
);
|
|
131
|
+
expect(result).toBe('7:00 PM - 10:00 PM');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns just start time if no end time', () => {
|
|
135
|
+
const result = formatTimeRange('2024-01-15T19:00:00Z', null, 'UTC');
|
|
136
|
+
expect(result).toBe('7:00 PM');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns just start time if end time is empty string', () => {
|
|
140
|
+
const result = formatTimeRange('2024-01-15T19:00:00Z', '', 'UTC');
|
|
141
|
+
expect(result).toBe('7:00 PM');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns empty string for invalid start', () => {
|
|
145
|
+
expect(formatTimeRange('invalid', '2024-01-15T22:00:00Z', 'UTC')).toBe('');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('formatCleanTimeRange', () => {
|
|
150
|
+
it('omits :00 minutes from times on the hour', () => {
|
|
151
|
+
const result = formatCleanTimeRange(
|
|
152
|
+
'2024-01-15T19:00:00Z',
|
|
153
|
+
'2024-01-15T22:00:00Z',
|
|
154
|
+
'UTC'
|
|
155
|
+
);
|
|
156
|
+
expect(result).toBe('7 PM - 10 PM');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('keeps minutes when not on the hour', () => {
|
|
160
|
+
const result = formatCleanTimeRange(
|
|
161
|
+
'2024-01-15T19:30:00Z',
|
|
162
|
+
'2024-01-15T22:00:00Z',
|
|
163
|
+
'UTC'
|
|
164
|
+
);
|
|
165
|
+
expect(result).toBe('7:30 PM - 10 PM');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns just start time if no end time', () => {
|
|
169
|
+
const result = formatCleanTimeRange('2024-01-15T19:00:00Z', null, 'UTC');
|
|
170
|
+
expect(result).toBe('7 PM');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns empty string for invalid start', () => {
|
|
174
|
+
expect(formatCleanTimeRange('invalid', '2024-01-15T22:00:00Z', 'UTC')).toBe('');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('formatDayOfWeek', () => {
|
|
179
|
+
it('returns short day name by default', () => {
|
|
180
|
+
const result = formatDayOfWeek('2024-01-15T12:00:00Z', 'UTC');
|
|
181
|
+
expect(result).toBe('Mon');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('returns long day name when short=false', () => {
|
|
185
|
+
const result = formatDayOfWeek('2024-01-15T12:00:00Z', 'UTC', false);
|
|
186
|
+
expect(result).toBe('Monday');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns empty string for invalid date', () => {
|
|
190
|
+
expect(formatDayOfWeek('invalid', 'UTC')).toBe('');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('formatMonth', () => {
|
|
195
|
+
it('returns short month name by default', () => {
|
|
196
|
+
const result = formatMonth('2024-01-15T12:00:00Z', 'UTC');
|
|
197
|
+
expect(result).toBe('Jan');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns long month name when short=false', () => {
|
|
201
|
+
const result = formatMonth('2024-01-15T12:00:00Z', 'UTC', false);
|
|
202
|
+
expect(result).toBe('January');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('handles different months', () => {
|
|
206
|
+
expect(formatMonth('2024-12-15T12:00:00Z', 'UTC')).toBe('Dec');
|
|
207
|
+
expect(formatMonth('2024-06-15T12:00:00Z', 'UTC')).toBe('Jun');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns empty string for invalid date', () => {
|
|
211
|
+
expect(formatMonth('invalid', 'UTC')).toBe('');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('formatHour', () => {
|
|
216
|
+
it('formats morning hours', () => {
|
|
217
|
+
expect(formatHour(0)).toBe('12 AM');
|
|
218
|
+
expect(formatHour(1)).toBe('1 AM');
|
|
219
|
+
expect(formatHour(9)).toBe('9 AM');
|
|
220
|
+
expect(formatHour(11)).toBe('11 AM');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('formats afternoon/evening hours', () => {
|
|
224
|
+
expect(formatHour(12)).toBe('12 PM');
|
|
225
|
+
expect(formatHour(13)).toBe('1 PM');
|
|
226
|
+
expect(formatHour(18)).toBe('6 PM');
|
|
227
|
+
expect(formatHour(23)).toBe('11 PM');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('getDateParts', () => {
|
|
232
|
+
it('returns all date parts correctly', () => {
|
|
233
|
+
const result = getDateParts('2024-01-15T12:00:00Z', 'UTC');
|
|
234
|
+
expect(result.day).toBe('Mon');
|
|
235
|
+
expect(result.month).toBe('Jan');
|
|
236
|
+
expect(result.date).toBe(15);
|
|
237
|
+
expect(result.year).toBe(2024);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns zeros for invalid date', () => {
|
|
241
|
+
const result = getDateParts('invalid', 'UTC');
|
|
242
|
+
expect(result.day).toBe('');
|
|
243
|
+
expect(result.month).toBe('');
|
|
244
|
+
expect(result.date).toBe(0);
|
|
245
|
+
expect(result.year).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns zeros for null', () => {
|
|
249
|
+
const result = getDateParts(null, 'UTC');
|
|
250
|
+
expect(result.day).toBe('');
|
|
251
|
+
expect(result.month).toBe('');
|
|
252
|
+
expect(result.date).toBe(0);
|
|
253
|
+
expect(result.year).toBe(0);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('isToday', () => {
|
|
258
|
+
it('returns true for today', () => {
|
|
259
|
+
const now = new Date();
|
|
260
|
+
const result = isToday(now.toISOString(), 'UTC');
|
|
261
|
+
expect(result).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns false for yesterday', () => {
|
|
265
|
+
const yesterday = new Date();
|
|
266
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
267
|
+
const result = isToday(yesterday.toISOString(), 'UTC');
|
|
268
|
+
expect(result).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('returns false for tomorrow', () => {
|
|
272
|
+
const tomorrow = new Date();
|
|
273
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
274
|
+
const result = isToday(tomorrow.toISOString(), 'UTC');
|
|
275
|
+
expect(result).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns false for invalid date', () => {
|
|
279
|
+
expect(isToday('invalid', 'UTC')).toBe(false);
|
|
280
|
+
expect(isToday(null, 'UTC')).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('formatNotificationTime', () => {
|
|
285
|
+
beforeEach(() => {
|
|
286
|
+
vi.useFakeTimers();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
afterEach(() => {
|
|
290
|
+
vi.useRealTimers();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('formats seconds ago for very recent', () => {
|
|
294
|
+
const now = new Date('2024-01-15T12:00:00Z');
|
|
295
|
+
vi.setSystemTime(now);
|
|
296
|
+
|
|
297
|
+
const thirtySecondsAgo = new Date(now.getTime() - 30000).toISOString();
|
|
298
|
+
const result = formatNotificationTime(thirtySecondsAgo, 'UTC');
|
|
299
|
+
expect(result).toBe('30 seconds ago');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('formats minutes ago', () => {
|
|
303
|
+
const now = new Date('2024-01-15T12:00:00Z');
|
|
304
|
+
vi.setSystemTime(now);
|
|
305
|
+
|
|
306
|
+
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60000).toISOString();
|
|
307
|
+
const result = formatNotificationTime(fiveMinutesAgo, 'UTC');
|
|
308
|
+
expect(result).toBe('5 minutes ago');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('formats hours ago', () => {
|
|
312
|
+
const now = new Date('2024-01-15T12:00:00Z');
|
|
313
|
+
vi.setSystemTime(now);
|
|
314
|
+
|
|
315
|
+
const threeHoursAgo = new Date(now.getTime() - 3 * 3600000).toISOString();
|
|
316
|
+
const result = formatNotificationTime(threeHoursAgo, 'UTC');
|
|
317
|
+
expect(result).toBe('3 hours ago');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('formats as date for yesterday', () => {
|
|
321
|
+
const now = new Date('2024-01-15T12:00:00Z');
|
|
322
|
+
vi.setSystemTime(now);
|
|
323
|
+
|
|
324
|
+
const yesterday = '2024-01-14T12:00:00Z';
|
|
325
|
+
const result = formatNotificationTime(yesterday, 'UTC');
|
|
326
|
+
expect(result).toBe('Jan 14, 2024 at 12:00 PM');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('returns empty string for invalid date', () => {
|
|
330
|
+
expect(formatNotificationTime('invalid', 'UTC')).toBe('');
|
|
331
|
+
expect(formatNotificationTime(null, 'UTC')).toBe('');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('DEFAULT_TIMEZONE', () => {
|
|
336
|
+
it('is UTC', () => {
|
|
337
|
+
expect(DEFAULT_TIMEZONE).toBe('UTC');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|