@jmruthers/pace-core 0.5.184 → 0.5.185
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/CHANGELOG.md +38 -0
- package/README.md +60 -1
- package/core-usage-manifest.json +312 -0
- package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
- package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
- package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-BABf6JCh.d.ts} +21 -10
- package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
- package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
- package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
- package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
- package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
- package/dist/{chunk-CSOFYHAG.js → chunk-AISXLWGZ.js} +374 -60
- package/dist/chunk-AISXLWGZ.js.map +1 -0
- package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
- package/dist/chunk-FXFJRTKI.js.map +1 -0
- package/dist/{chunk-NQPMQGS2.js → chunk-HC67NW5K.js} +379 -359
- package/dist/chunk-HC67NW5K.js.map +1 -0
- package/dist/chunk-HESYZWZW.js +388 -0
- package/dist/chunk-HESYZWZW.js.map +1 -0
- package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
- package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
- package/dist/{chunk-UHNYIBXL.js → chunk-IXSNYUCT.js} +1 -1
- package/dist/chunk-IXSNYUCT.js.map +1 -0
- package/dist/{chunk-MI7HBHN3.js → chunk-MX3EIJGQ.js} +4 -3
- package/dist/{chunk-MI7HBHN3.js.map → chunk-MX3EIJGQ.js.map} +1 -1
- package/dist/{chunk-PWAHJW4G.js → chunk-OKI34GZD.js} +86 -33
- package/dist/chunk-OKI34GZD.js.map +1 -0
- package/dist/{chunk-W22JP75J.js → chunk-STTZQK2I.js} +3 -3
- package/dist/chunk-THRPYOFK.js +215 -0
- package/dist/chunk-THRPYOFK.js.map +1 -0
- package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
- package/dist/chunk-U6WNSFX5.js.map +1 -0
- package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
- package/dist/components.d.ts +182 -6
- package/dist/components.js +157 -11
- package/dist/components.js.map +1 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
- package/dist/{file-reference-D06mEEWW.d.ts → file-reference-BjR39ktt.d.ts} +7 -1
- package/dist/hooks.d.ts +7 -14
- package/dist/hooks.js +10 -22
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -1
- package/dist/rbac/index.d.ts +205 -14
- package/dist/rbac/index.js +28 -6
- package/dist/timezone-_pgH8qrY.d.ts +530 -0
- package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-CvnC3d-e.d.ts} +113 -2
- package/dist/utils.d.ts +109 -151
- package/dist/utils.js +128 -138
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +60 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +178 -0
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +54 -0
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +18 -2
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +30 -0
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
- package/docs/api/interfaces/DatabaseIssue.md +41 -0
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +6 -6
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +24 -8
- package/docs/api/interfaces/FileUploadProps.md +24 -13
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +62 -0
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +52 -0
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +4 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +5 -5
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +41 -0
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
- package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +738 -42
- package/docs/api-reference/hooks.md +111 -0
- package/docs/api-reference/rpc-functions.md +1 -1
- package/docs/api-reference/utilities.md +184 -0
- package/docs/getting-started/installation-guide.md +75 -16
- package/docs/getting-started/quick-start.md +61 -11
- package/docs/implementation-guides/authentication.md +88 -12
- package/docs/implementation-guides/file-reference-system.md +2 -1
- package/docs/implementation-guides/file-upload-storage.md +21 -0
- package/docs/rbac/README.md +1 -0
- package/docs/rbac/compliance/compliance-guide.md +544 -0
- package/docs/rbac/getting-started.md +158 -33
- package/docs/standards/pace-core-compliance.md +432 -0
- package/eslint-config-pace-core.cjs +93 -0
- package/package.json +15 -3
- package/scripts/analyze-bundle.js +232 -0
- package/scripts/build-css.js +56 -0
- package/scripts/build-docs-incremental.js +1015 -0
- package/scripts/check-pace-core-compliance.cjs +2353 -0
- package/scripts/generate-docs.js +157 -0
- package/scripts/setup-build-cache.js +73 -0
- package/scripts/utils/command-runner.js +131 -0
- package/scripts/utils/env.js +33 -0
- package/scripts/utils/index.js +10 -0
- package/scripts/utils/logger.js +88 -0
- package/scripts/utils/path-helpers.js +37 -0
- package/scripts/validate-formats.js +133 -0
- package/scripts/validate-master.js +155 -0
- package/scripts/validate-pre-publish.js +140 -0
- package/scripts/validate-theme.js +142 -0
- package/src/components/Calendar/Calendar.tsx +8 -1
- package/src/components/Card/Card.tsx +47 -8
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
- package/src/components/DatePickerWithTimezone/README.md +135 -0
- package/src/components/DatePickerWithTimezone/index.ts +10 -0
- package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
- package/src/components/DateTimeField/DateTimeField.tsx +232 -0
- package/src/components/DateTimeField/README.md +148 -0
- package/src/components/DateTimeField/index.ts +10 -0
- package/src/components/FileUpload/FileUpload.tsx +3 -0
- package/src/components/Header/Header.test.tsx +47 -18
- package/src/components/Header/Header.tsx +24 -6
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
- package/src/components/PaceAppLayout/README.md +9 -0
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +12 -4
- package/src/components/index.ts +8 -0
- package/src/eslint-rules/pace-core-compliance.cjs +406 -0
- package/src/eslint-rules/pace-core-compliance.js +640 -0
- package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useFileReference.test.ts +1 -0
- package/src/hooks/useFormDialog.ts +147 -0
- package/src/index.ts +27 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
- package/src/rbac/compliance/database-validator.ts +165 -0
- package/src/rbac/compliance/index.ts +38 -0
- package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
- package/src/rbac/compliance/runtime-compliance.ts +77 -0
- package/src/rbac/compliance/setup-validator.ts +131 -0
- package/src/rbac/components/PagePermissionGuard.tsx +8 -64
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
- package/src/rbac/docs/event-based-apps.md +285 -0
- package/src/rbac/errors.ts +11 -0
- package/src/rbac/hooks/useRoleManagement.ts +292 -12
- package/src/rbac/index.ts +30 -0
- package/src/services/OrganisationService.ts +4 -0
- package/src/types/file-reference.ts +6 -0
- package/src/utils/__tests__/timezone.test.ts +345 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +2 -0
- package/src/utils/file-reference/index.ts +1 -0
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
- package/src/utils/formatting/formatting.ts +179 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/location/index.ts +16 -0
- package/src/utils/location/location.test.ts +286 -0
- package/src/utils/location/location.ts +175 -0
- package/src/utils/timezone/index.ts +17 -0
- package/src/utils/timezone/timezone.test.ts +349 -0
- package/src/utils/timezone/timezone.ts +281 -0
- package/dist/chunk-CSOFYHAG.js.map +0 -1
- package/dist/chunk-FUEYYMX5.js.map +0 -1
- package/dist/chunk-HKIT6O7W.js +0 -198
- package/dist/chunk-HKIT6O7W.js.map +0 -1
- package/dist/chunk-KUEN3HFB.js +0 -94
- package/dist/chunk-KUEN3HFB.js.map +0 -1
- package/dist/chunk-M7W4CP3M.js.map +0 -1
- package/dist/chunk-NQPMQGS2.js.map +0 -1
- package/dist/chunk-PWAHJW4G.js.map +0 -1
- package/dist/chunk-UHNYIBXL.js.map +0 -1
- package/dist/formatting-5wETwiGF.d.ts +0 -162
- /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
- /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
- /package/dist/{chunk-W22JP75J.js.map → chunk-STTZQK2I.js.map} +0 -0
- /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Timezone Utilities Exports
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Timezone
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
formatInTimeZone,
|
|
10
|
+
getTimezoneAbbreviation,
|
|
11
|
+
formatTimeInTimeZone,
|
|
12
|
+
getUserTimeZone,
|
|
13
|
+
toZonedTime,
|
|
14
|
+
fromZonedTime,
|
|
15
|
+
roundToNearestMinutes,
|
|
16
|
+
getTimeZoneDifference
|
|
17
|
+
} from './timezone';
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Timezone Utilities Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Timezone/__tests__
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for timezone utility functions.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and error handling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
formatInTimeZone,
|
|
14
|
+
getTimezoneAbbreviation,
|
|
15
|
+
formatTimeInTimeZone,
|
|
16
|
+
getUserTimeZone,
|
|
17
|
+
toZonedTime,
|
|
18
|
+
fromZonedTime,
|
|
19
|
+
roundToNearestMinutes,
|
|
20
|
+
getTimeZoneDifference
|
|
21
|
+
} from './timezone';
|
|
22
|
+
|
|
23
|
+
describe('Timezone Utilities', () => {
|
|
24
|
+
describe('formatInTimeZone', () => {
|
|
25
|
+
it('formats a Date object in a specific timezone', () => {
|
|
26
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
27
|
+
const result = formatInTimeZone(date, 'America/New_York', 'MMM dd, yyyy HH:mm');
|
|
28
|
+
expect(result).toMatch(/Jan 15, 2024/);
|
|
29
|
+
expect(result).toMatch(/\d{2}:\d{2}/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('formats an ISO string in a specific timezone', () => {
|
|
33
|
+
const result = formatInTimeZone('2024-01-15T10:00:00Z', 'America/New_York', 'MMM dd, yyyy');
|
|
34
|
+
expect(result).toMatch(/Jan 15, 2024/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('formats a timestamp in a specific timezone', () => {
|
|
38
|
+
const timestamp = new Date('2024-01-15T10:00:00Z').getTime();
|
|
39
|
+
const result = formatInTimeZone(timestamp, 'America/New_York', 'yyyy-MM-dd');
|
|
40
|
+
expect(result).toBe('2024-01-15');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns "Invalid date" for invalid date input', () => {
|
|
44
|
+
const result = formatInTimeZone(new Date('invalid'), 'America/New_York', 'MMM dd, yyyy');
|
|
45
|
+
expect(result).toBe('Invalid date');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns "Invalid date" for invalid timezone', () => {
|
|
49
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
50
|
+
const result = formatInTimeZone(date, '', 'MMM dd, yyyy');
|
|
51
|
+
expect(result).toBe('Invalid date');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns "Invalid date" for non-string timezone', () => {
|
|
55
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
56
|
+
// @ts-expect-error - Testing invalid input
|
|
57
|
+
const result = formatInTimeZone(date, null, 'MMM dd, yyyy');
|
|
58
|
+
expect(result).toBe('Invalid date');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles different timezones correctly', () => {
|
|
62
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
63
|
+
const ny = formatInTimeZone(date, 'America/New_York', 'HH:mm');
|
|
64
|
+
const la = formatInTimeZone(date, 'America/Los_Angeles', 'HH:mm');
|
|
65
|
+
expect(ny).not.toBe(la);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('getTimezoneAbbreviation', () => {
|
|
70
|
+
it('returns timezone abbreviation for valid date and timezone', () => {
|
|
71
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
72
|
+
const result = getTimezoneAbbreviation(date, 'America/New_York');
|
|
73
|
+
expect(typeof result).toBe('string');
|
|
74
|
+
expect(result.length).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns timezone name as fallback when abbreviation unavailable', () => {
|
|
78
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
79
|
+
const result = getTimezoneAbbreviation(date, 'America/New_York');
|
|
80
|
+
expect(result).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns timezone string for invalid date', () => {
|
|
84
|
+
const result = getTimezoneAbbreviation(new Date('invalid'), 'America/New_York');
|
|
85
|
+
expect(result).toBe('America/New_York');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns "UTC" for empty timezone', () => {
|
|
89
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
90
|
+
const result = getTimezoneAbbreviation(date, '');
|
|
91
|
+
expect(result).toBe('UTC');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('handles invalid timezone gracefully', () => {
|
|
95
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
96
|
+
const result = getTimezoneAbbreviation(date, 'Invalid/Timezone');
|
|
97
|
+
expect(typeof result).toBe('string');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('formatTimeInTimeZone', () => {
|
|
102
|
+
it('formats time only in specific timezone', () => {
|
|
103
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
104
|
+
const result = formatTimeInTimeZone(date, 'America/New_York');
|
|
105
|
+
expect(result).toMatch(/^\d{2}:\d{2}$/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('formats ISO string time in timezone', () => {
|
|
109
|
+
const result = formatTimeInTimeZone('2024-01-15T10:00:00Z', 'America/New_York');
|
|
110
|
+
expect(result).toMatch(/^\d{2}:\d{2}$/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns "Invalid date" for invalid input', () => {
|
|
114
|
+
const result = formatTimeInTimeZone(new Date('invalid'), 'America/New_York');
|
|
115
|
+
expect(result).toBe('Invalid date');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getUserTimeZone', () => {
|
|
120
|
+
it('returns IANA timezone string', () => {
|
|
121
|
+
const result = getUserTimeZone();
|
|
122
|
+
expect(typeof result).toBe('string');
|
|
123
|
+
expect(result.length).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns "UTC" as fallback when Intl is unavailable', () => {
|
|
127
|
+
const originalIntl = global.Intl;
|
|
128
|
+
// @ts-expect-error - Testing fallback behavior
|
|
129
|
+
global.Intl = undefined;
|
|
130
|
+
const result = getUserTimeZone();
|
|
131
|
+
expect(result).toBe('UTC');
|
|
132
|
+
global.Intl = originalIntl;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns "UTC" on error', () => {
|
|
136
|
+
const originalDateTimeFormat = Intl.DateTimeFormat;
|
|
137
|
+
// @ts-expect-error - Testing error handling
|
|
138
|
+
Intl.DateTimeFormat = () => {
|
|
139
|
+
throw new Error('Test error');
|
|
140
|
+
};
|
|
141
|
+
const result = getUserTimeZone();
|
|
142
|
+
expect(result).toBe('UTC');
|
|
143
|
+
Intl.DateTimeFormat = originalDateTimeFormat;
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('toZonedTime', () => {
|
|
148
|
+
it('converts UTC date to timezone local time', () => {
|
|
149
|
+
const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
150
|
+
const result = toZonedTime(utcDate, 'America/New_York');
|
|
151
|
+
expect(result).toBeInstanceOf(Date);
|
|
152
|
+
expect(result.getTime()).not.toBe(utcDate.getTime());
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns original date for invalid timezone', () => {
|
|
156
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
157
|
+
const result = toZonedTime(date, '');
|
|
158
|
+
expect(result).toBe(date);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('returns original date for invalid date', () => {
|
|
162
|
+
const invalidDate = new Date('invalid');
|
|
163
|
+
const result = toZonedTime(invalidDate, 'America/New_York');
|
|
164
|
+
expect(result).toBe(invalidDate);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('handles different timezones correctly', () => {
|
|
168
|
+
const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
169
|
+
const ny = toZonedTime(utcDate, 'America/New_York');
|
|
170
|
+
const la = toZonedTime(utcDate, 'America/Los_Angeles');
|
|
171
|
+
expect(ny.getTime()).not.toBe(la.getTime());
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns original date on error', () => {
|
|
175
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
176
|
+
// @ts-expect-error - Testing error handling
|
|
177
|
+
const result = toZonedTime(date, null);
|
|
178
|
+
expect(result).toBe(date);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('fromZonedTime', () => {
|
|
183
|
+
it('converts local time in timezone to UTC', () => {
|
|
184
|
+
const localDate = new Date(2024, 0, 15, 10, 0); // Jan 15, 2024 10:00 AM local
|
|
185
|
+
const result = fromZonedTime(localDate, 'America/New_York');
|
|
186
|
+
expect(result).toBeInstanceOf(Date);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns original date for invalid timezone', () => {
|
|
190
|
+
const date = new Date(2024, 0, 15, 10, 0);
|
|
191
|
+
const result = fromZonedTime(date, '');
|
|
192
|
+
expect(result).toBe(date);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns original date for invalid date', () => {
|
|
196
|
+
const invalidDate = new Date('invalid');
|
|
197
|
+
const result = fromZonedTime(invalidDate, 'America/New_York');
|
|
198
|
+
expect(result).toBe(invalidDate);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('handles round-trip conversion', () => {
|
|
202
|
+
const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
203
|
+
const zoned = toZonedTime(utcDate, 'America/New_York');
|
|
204
|
+
const backToUtc = fromZonedTime(zoned, 'America/New_York');
|
|
205
|
+
// Should be close to original (within a few milliseconds due to DST handling)
|
|
206
|
+
expect(Math.abs(backToUtc.getTime() - utcDate.getTime())).toBeLessThan(1000);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns original date on error', () => {
|
|
210
|
+
const date = new Date(2024, 0, 15, 10, 0);
|
|
211
|
+
// @ts-expect-error - Testing error handling
|
|
212
|
+
const result = fromZonedTime(date, null);
|
|
213
|
+
expect(result).toBe(date);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('roundToNearestMinutes', () => {
|
|
218
|
+
it('rounds to nearest 5 minutes by default', () => {
|
|
219
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
220
|
+
const result = roundToNearestMinutes(date);
|
|
221
|
+
expect(result.getMinutes() % 5).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rounds to nearest specified minutes', () => {
|
|
225
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
226
|
+
const result = roundToNearestMinutes(date, 10);
|
|
227
|
+
expect(result.getMinutes() % 10).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rounds up when closer to next interval', () => {
|
|
231
|
+
const date = new Date('2024-01-15T10:28:00Z');
|
|
232
|
+
const result = roundToNearestMinutes(date, 5);
|
|
233
|
+
expect(result.getMinutes()).toBe(30);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('rounds down when closer to previous interval', () => {
|
|
237
|
+
const date = new Date('2024-01-15T10:22:00Z');
|
|
238
|
+
const result = roundToNearestMinutes(date, 5);
|
|
239
|
+
expect(result.getMinutes()).toBe(20);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns original date for invalid date', () => {
|
|
243
|
+
const invalidDate = new Date('invalid');
|
|
244
|
+
const result = roundToNearestMinutes(invalidDate, 5);
|
|
245
|
+
expect(result).toBe(invalidDate);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns original date for invalid minutesStep', () => {
|
|
249
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
250
|
+
const result = roundToNearestMinutes(date, 0);
|
|
251
|
+
expect(date).toBe(date);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns original date for negative minutesStep', () => {
|
|
255
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
256
|
+
const result = roundToNearestMinutes(date, -5);
|
|
257
|
+
expect(result).toBe(date);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns original date for non-integer minutesStep', () => {
|
|
261
|
+
const date = new Date('2024-01-15T10:23:00Z');
|
|
262
|
+
const result = roundToNearestMinutes(date, 5.5);
|
|
263
|
+
expect(result).toBe(date);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('handles edge case of exact minute', () => {
|
|
267
|
+
const date = new Date('2024-01-15T10:25:00Z');
|
|
268
|
+
const result = roundToNearestMinutes(date, 5);
|
|
269
|
+
expect(result.getMinutes()).toBe(25);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('getTimeZoneDifference', () => {
|
|
274
|
+
it('calculates difference between two timezones', () => {
|
|
275
|
+
const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
|
|
276
|
+
expect(typeof result).toBe('number');
|
|
277
|
+
// Los Angeles is typically 3 hours behind New York
|
|
278
|
+
expect(result).toBeLessThan(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns 0 for same timezone', () => {
|
|
282
|
+
const result = getTimeZoneDifference('America/New_York', 'America/New_York');
|
|
283
|
+
expect(result).toBe(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns 0 for invalid timezones', () => {
|
|
287
|
+
const result = getTimeZoneDifference('', 'America/New_York');
|
|
288
|
+
expect(result).toBe(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns 0 for non-string timezones', () => {
|
|
292
|
+
// @ts-expect-error - Testing invalid input
|
|
293
|
+
const result = getTimeZoneDifference(null, 'America/New_York');
|
|
294
|
+
expect(result).toBe(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('handles DST correctly', () => {
|
|
298
|
+
// Test in summer (EDT vs PDT = 3 hours)
|
|
299
|
+
const summerDate = new Date('2024-07-15T10:00:00Z');
|
|
300
|
+
const result = getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
|
|
301
|
+
// Should be negative (LA behind NY)
|
|
302
|
+
expect(result).toBeLessThan(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('returns 0 on error', () => {
|
|
306
|
+
// This test verifies error handling path
|
|
307
|
+
const result = getTimeZoneDifference('Invalid/Timezone1', 'Invalid/Timezone2');
|
|
308
|
+
expect(result).toBe(0);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('Edge Cases', () => {
|
|
313
|
+
it('handles null and undefined inputs gracefully', () => {
|
|
314
|
+
// @ts-expect-error - Testing edge cases
|
|
315
|
+
expect(formatInTimeZone(null, 'America/New_York', 'yyyy-MM-dd')).toBe('Invalid date');
|
|
316
|
+
// @ts-expect-error - Testing edge cases
|
|
317
|
+
expect(formatInTimeZone(undefined, 'America/New_York', 'yyyy-MM-dd')).toBe('Invalid date');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('handles extreme dates', () => {
|
|
321
|
+
const farFuture = new Date('2099-12-31T23:59:59Z');
|
|
322
|
+
const result = formatInTimeZone(farFuture, 'America/New_York', 'yyyy-MM-dd');
|
|
323
|
+
expect(result).toMatch(/2099-12-31/);
|
|
324
|
+
|
|
325
|
+
const farPast = new Date('1900-01-01T00:00:00Z');
|
|
326
|
+
const result2 = formatInTimeZone(farPast, 'America/New_York', 'yyyy-MM-dd');
|
|
327
|
+
// Timezone conversion may shift the date to previous day due to timezone offset
|
|
328
|
+
// Accept either 1900-01-01 or 1899-12-31 depending on timezone offset
|
|
329
|
+
expect(result2).toMatch(/1899-12-31|1900-01-01/);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('handles various timezone formats', () => {
|
|
333
|
+
const date = new Date('2024-01-15T10:00:00Z');
|
|
334
|
+
const timezones = [
|
|
335
|
+
'America/New_York',
|
|
336
|
+
'Europe/London',
|
|
337
|
+
'Asia/Tokyo',
|
|
338
|
+
'Australia/Sydney',
|
|
339
|
+
'UTC'
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
timezones.forEach(tz => {
|
|
343
|
+
const result = formatInTimeZone(date, tz, 'yyyy-MM-dd');
|
|
344
|
+
expect(result).toBe('2024-01-15');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Timezone Utilities
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Timezone
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive timezone conversion and formatting utilities using date-fns-tz and native Intl APIs.
|
|
8
|
+
* Provides functions for timezone-aware date operations, conversions, and formatting.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Format dates in specific timezones
|
|
12
|
+
* - Get timezone abbreviations
|
|
13
|
+
* - Convert between UTC and timezone local times
|
|
14
|
+
* - Round dates to nearest minutes
|
|
15
|
+
* - Calculate timezone differences
|
|
16
|
+
* - Get user's browser timezone
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { toZonedTime, fromZonedTime, formatInTimeZone, getUserTimeZone } from '@jmruthers/pace-core/utils/timezone';
|
|
21
|
+
*
|
|
22
|
+
* // Convert UTC to local timezone
|
|
23
|
+
* const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
24
|
+
* const localDate = toZonedTime(utcDate, 'America/New_York');
|
|
25
|
+
*
|
|
26
|
+
* // Convert local time to UTC
|
|
27
|
+
* const localInput = new Date(2024, 0, 15, 10, 0);
|
|
28
|
+
* const utcDate = fromZonedTime(localInput, 'America/New_York');
|
|
29
|
+
*
|
|
30
|
+
* // Format with timezone
|
|
31
|
+
* const formatted = formatInTimeZone(utcDate, 'America/New_York', 'MMM dd, yyyy HH:mm');
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { format, parseISO, addMinutes, differenceInHours, isValid } from 'date-fns';
|
|
36
|
+
import { formatInTimeZone as fnsFormatInTimeZone, toZonedTime as fnsToZonedTime, fromZonedTime as fnsFromZonedTime } from 'date-fns-tz';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format a date in a specific timezone using date-fns format strings
|
|
40
|
+
*
|
|
41
|
+
* @param date - Date to format (Date object, ISO string, or timestamp)
|
|
42
|
+
* @param timeZone - IANA timezone string (e.g., 'America/New_York')
|
|
43
|
+
* @param formatStr - date-fns format string (e.g., 'MMM dd, yyyy HH:mm')
|
|
44
|
+
* @returns Formatted date string or 'Invalid date' on error
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* formatInTimeZone(new Date('2024-01-15T10:00:00Z'), 'America/New_York', 'MMM dd, yyyy HH:mm');
|
|
49
|
+
* // "Jan 15, 2024 05:00"
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function formatInTimeZone(
|
|
53
|
+
date: Date | string | number,
|
|
54
|
+
timeZone: string,
|
|
55
|
+
formatStr: string
|
|
56
|
+
): string {
|
|
57
|
+
try {
|
|
58
|
+
if (!timeZone || typeof timeZone !== 'string') {
|
|
59
|
+
return 'Invalid date';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let dateObj: Date;
|
|
63
|
+
if (typeof date === 'string') {
|
|
64
|
+
dateObj = parseISO(date);
|
|
65
|
+
} else if (typeof date === 'number') {
|
|
66
|
+
dateObj = new Date(date);
|
|
67
|
+
} else {
|
|
68
|
+
dateObj = date;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isValid(dateObj)) {
|
|
72
|
+
return 'Invalid date';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return fnsFormatInTimeZone(dateObj, timeZone, formatStr);
|
|
76
|
+
} catch {
|
|
77
|
+
return 'Invalid date';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the timezone abbreviation (EST, PST, etc.) for a date in a specific timezone
|
|
83
|
+
*
|
|
84
|
+
* @param date - Date to get abbreviation for
|
|
85
|
+
* @param timeZone - IANA timezone string
|
|
86
|
+
* @returns Timezone abbreviation or timezone name on error
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* getTimezoneAbbreviation(new Date(), 'America/New_York');
|
|
91
|
+
* // "EST" or "EDT" depending on date
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function getTimezoneAbbreviation(date: Date, timeZone: string): string {
|
|
95
|
+
try {
|
|
96
|
+
if (!timeZone || typeof timeZone !== 'string') {
|
|
97
|
+
return timeZone || 'UTC';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!isValid(date)) {
|
|
101
|
+
return timeZone;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
105
|
+
timeZone,
|
|
106
|
+
timeZoneName: 'short'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const parts = formatter.formatToParts(date);
|
|
110
|
+
const timeZoneName = parts.find(part => part.type === 'timeZoneName');
|
|
111
|
+
|
|
112
|
+
return timeZoneName?.value || timeZone;
|
|
113
|
+
} catch {
|
|
114
|
+
return timeZone || 'UTC';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Format time only in a specific timezone
|
|
120
|
+
*
|
|
121
|
+
* @param date - Date to format
|
|
122
|
+
* @param timeZone - IANA timezone string
|
|
123
|
+
* @returns Formatted time string (HH:mm format) or 'Invalid date' on error
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* formatTimeInTimeZone(new Date('2024-01-15T10:00:00Z'), 'America/New_York');
|
|
128
|
+
* // "05:00"
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function formatTimeInTimeZone(date: Date | string, timeZone: string): string {
|
|
132
|
+
return formatInTimeZone(date, timeZone, 'HH:mm');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the user's local timezone from the browser
|
|
137
|
+
*
|
|
138
|
+
* @returns IANA timezone string (e.g., 'America/New_York')
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* const userTz = getUserTimeZone();
|
|
143
|
+
* // "America/New_York" or user's actual timezone
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function getUserTimeZone(): string {
|
|
147
|
+
try {
|
|
148
|
+
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
|
149
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
150
|
+
}
|
|
151
|
+
return 'UTC';
|
|
152
|
+
} catch {
|
|
153
|
+
return 'UTC';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Convert a UTC date to a specific timezone's local time representation
|
|
159
|
+
*
|
|
160
|
+
* @param date - UTC date to convert
|
|
161
|
+
* @param timezone - IANA timezone string
|
|
162
|
+
* @returns Date object representing local time in the timezone
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* const utcDate = new Date('2024-01-15T10:00:00Z');
|
|
167
|
+
* const localDate = toZonedTime(utcDate, 'America/New_York');
|
|
168
|
+
* // Returns Date object representing 5:00 AM in New York
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export function toZonedTime(date: Date, timezone: string): Date {
|
|
172
|
+
try {
|
|
173
|
+
if (!timezone || typeof timezone !== 'string') {
|
|
174
|
+
return date;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!isValid(date)) {
|
|
178
|
+
return date;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return fnsToZonedTime(date, timezone);
|
|
182
|
+
} catch {
|
|
183
|
+
return date;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convert a local time in a specific timezone to UTC
|
|
189
|
+
*
|
|
190
|
+
* @param localDate - Local date in the timezone
|
|
191
|
+
* @param timezone - IANA timezone string
|
|
192
|
+
* @returns Date object in UTC
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* const localDate = new Date(2024, 0, 15, 10, 0); // Jan 15, 2024 10:00 AM local
|
|
197
|
+
* const utcDate = fromZonedTime(localDate, 'America/New_York');
|
|
198
|
+
* // Returns Date object representing 3:00 PM UTC (if EST) or 2:00 PM UTC (if EDT)
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function fromZonedTime(localDate: Date, timezone: string): Date {
|
|
202
|
+
try {
|
|
203
|
+
if (!timezone || typeof timezone !== 'string') {
|
|
204
|
+
return localDate;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!isValid(localDate)) {
|
|
208
|
+
return localDate;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return fnsFromZonedTime(localDate, timezone);
|
|
212
|
+
} catch {
|
|
213
|
+
return localDate;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Round a date to the nearest X minutes
|
|
219
|
+
*
|
|
220
|
+
* @param date - Date to round
|
|
221
|
+
* @param minutesStep - Number of minutes to round to (default: 5)
|
|
222
|
+
* @returns Rounded date
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* const date = new Date('2024-01-15T10:23:00Z');
|
|
227
|
+
* roundToNearestMinutes(date, 5);
|
|
228
|
+
* // Returns Date object for 10:25:00
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export function roundToNearestMinutes(date: Date, minutesStep: number = 5): Date {
|
|
232
|
+
try {
|
|
233
|
+
if (!isValid(date)) {
|
|
234
|
+
return date;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (minutesStep <= 0 || !Number.isInteger(minutesStep)) {
|
|
238
|
+
return date;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const minutes = date.getMinutes();
|
|
242
|
+
const roundedMinutes = Math.round(minutes / minutesStep) * minutesStep;
|
|
243
|
+
const diff = roundedMinutes - minutes;
|
|
244
|
+
|
|
245
|
+
return addMinutes(date, diff);
|
|
246
|
+
} catch {
|
|
247
|
+
return date;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Calculate the time difference between two timezones in hours
|
|
253
|
+
*
|
|
254
|
+
* @param fromTimeZone - Source timezone (IANA string)
|
|
255
|
+
* @param toTimeZone - Target timezone (IANA string)
|
|
256
|
+
* @returns Difference in hours (positive if toTimeZone is ahead)
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* getTimeZoneDifference('America/New_York', 'America/Los_Angeles');
|
|
261
|
+
* // -3 (Los Angeles is 3 hours behind New York)
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
export function getTimeZoneDifference(fromTimeZone: string, toTimeZone: string): number {
|
|
265
|
+
try {
|
|
266
|
+
if (!fromTimeZone || !toTimeZone || typeof fromTimeZone !== 'string' || typeof toTimeZone !== 'string') {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const now = new Date();
|
|
271
|
+
const fromDate = toZonedTime(now, fromTimeZone);
|
|
272
|
+
const toDate = toZonedTime(now, toTimeZone);
|
|
273
|
+
|
|
274
|
+
const diff = differenceInHours(toDate, fromDate);
|
|
275
|
+
// Handle NaN case (differenceInHours can return NaN for invalid dates)
|
|
276
|
+
return isNaN(diff) ? 0 : diff;
|
|
277
|
+
} catch {
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|