@jmruthers/pace-core 0.5.183 → 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-QETLRQI6.js → chunk-HC67NW5K.js} +380 -360
- 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/PaceLoginPage/PaceLoginPage.tsx +1 -1
- 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-PWAHJW4G.js.map +0 -1
- package/dist/chunk-QETLRQI6.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
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Utility functions for formatting data in the application
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { parseISO, isValid } from 'date-fns';
|
|
6
|
+
import { formatInTimeZone, getTimezoneAbbreviation } from '../timezone';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Format a date as a readable string in "dd mmm yyyy" format (e.g., "15 Jun 2024")
|
|
7
10
|
*/
|
|
@@ -168,3 +171,179 @@ export function formatFileSize(bytes: number): string {
|
|
|
168
171
|
|
|
169
172
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
170
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Options for formatting date/time with timezone
|
|
177
|
+
*/
|
|
178
|
+
export interface DateTimeFormatOptions {
|
|
179
|
+
/**
|
|
180
|
+
* Include timezone abbreviation (default: true)
|
|
181
|
+
*/
|
|
182
|
+
includeTimezone?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Custom format string (default: 'MMM dd, yyyy HH:mm')
|
|
185
|
+
*/
|
|
186
|
+
format?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Format a UTC date for display in a specific timezone
|
|
191
|
+
*
|
|
192
|
+
* @param utcDate - UTC date (ISO string, Date object, or undefined)
|
|
193
|
+
* @param timezone - IANA timezone string (e.g., 'America/New_York')
|
|
194
|
+
* @param options - Formatting options
|
|
195
|
+
* @returns Formatted date string or empty string if invalid
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York');
|
|
200
|
+
* // "Jan 15, 2024 05:00 (EST)"
|
|
201
|
+
*
|
|
202
|
+
* formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York', { includeTimezone: false });
|
|
203
|
+
* // "Jan 15, 2024 05:00"
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function formatDateTimeForDisplay(
|
|
207
|
+
utcDate: string | Date | undefined,
|
|
208
|
+
timezone: string | undefined,
|
|
209
|
+
options: DateTimeFormatOptions = {}
|
|
210
|
+
): string {
|
|
211
|
+
if (!utcDate) {
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!timezone) {
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const { includeTimezone = true, format: formatStr = 'MMM dd, yyyy HH:mm' } = options;
|
|
221
|
+
|
|
222
|
+
let dateObj: Date;
|
|
223
|
+
if (typeof utcDate === 'string') {
|
|
224
|
+
dateObj = parseISO(utcDate);
|
|
225
|
+
} else {
|
|
226
|
+
dateObj = utcDate;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!isValid(dateObj)) {
|
|
230
|
+
return '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const formatted = formatInTimeZone(dateObj, timezone, formatStr);
|
|
234
|
+
|
|
235
|
+
if (includeTimezone) {
|
|
236
|
+
const tzAbbr = getTimezoneAbbreviation(dateObj, timezone);
|
|
237
|
+
return `${formatted} (${tzAbbr})`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return formatted;
|
|
241
|
+
} catch {
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Format a UTC date for display (date only, no time)
|
|
248
|
+
*
|
|
249
|
+
* @param utcDate - UTC date (ISO string, Date object, or undefined)
|
|
250
|
+
* @returns Formatted date string or empty string if invalid
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* formatDateOnlyForDisplay('2024-01-15T10:00:00Z');
|
|
255
|
+
* // "15 January 2024"
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export function formatDateOnlyForDisplay(utcDate: string | Date | undefined): string {
|
|
259
|
+
if (!utcDate) {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
let dateObj: Date;
|
|
265
|
+
if (typeof utcDate === 'string') {
|
|
266
|
+
dateObj = parseISO(utcDate);
|
|
267
|
+
} else {
|
|
268
|
+
dateObj = utcDate;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!isValid(dateObj)) {
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Use 'en-GB' locale for "dd mmm yyyy" format
|
|
276
|
+
return dateObj.toLocaleDateString('en-GB', {
|
|
277
|
+
year: 'numeric',
|
|
278
|
+
month: 'long',
|
|
279
|
+
day: 'numeric'
|
|
280
|
+
});
|
|
281
|
+
} catch {
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Format a UTC date for table display (compact format with timezone)
|
|
288
|
+
*
|
|
289
|
+
* @param utcDate - UTC date (ISO string, Date object, or undefined)
|
|
290
|
+
* @param timezone - IANA timezone string
|
|
291
|
+
* @returns Formatted date string or empty string if invalid
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* formatDateTimeForTable('2024-01-15T10:00:00Z', 'America/New_York');
|
|
296
|
+
* // "Jan 15, 2024 05:00 (EST)"
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export function formatDateTimeForTable(
|
|
300
|
+
utcDate: string | Date | undefined,
|
|
301
|
+
timezone: string | undefined
|
|
302
|
+
): string {
|
|
303
|
+
return formatDateTimeForDisplay(utcDate, timezone, {
|
|
304
|
+
includeTimezone: true,
|
|
305
|
+
format: 'MMM dd, yyyy HH:mm'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Format a UTC date for map display (compact format)
|
|
311
|
+
*
|
|
312
|
+
* @param utcDate - UTC date (ISO string, Date object, or undefined)
|
|
313
|
+
* @param timezone - IANA timezone string
|
|
314
|
+
* @returns Formatted date string or empty string if invalid
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* formatDateTimeForMap('2024-01-15T10:00:00Z', 'America/New_York');
|
|
319
|
+
* // "Jan 15, 05:00 EST"
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export function formatDateTimeForMap(
|
|
323
|
+
utcDate: string | Date | undefined,
|
|
324
|
+
timezone: string | undefined
|
|
325
|
+
): string {
|
|
326
|
+
if (!utcDate || !timezone) {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
let dateObj: Date;
|
|
332
|
+
if (typeof utcDate === 'string') {
|
|
333
|
+
dateObj = parseISO(utcDate);
|
|
334
|
+
} else {
|
|
335
|
+
dateObj = utcDate;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!isValid(dateObj)) {
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const formatted = formatInTimeZone(dateObj, timezone, 'MMM dd, HH:mm');
|
|
343
|
+
const tzAbbr = getTimezoneAbbreviation(dateObj, timezone);
|
|
344
|
+
|
|
345
|
+
return `${formatted} ${tzAbbr}`;
|
|
346
|
+
} catch {
|
|
347
|
+
return '';
|
|
348
|
+
}
|
|
349
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -120,8 +120,13 @@ export {
|
|
|
120
120
|
formatNumber,
|
|
121
121
|
formatPercent,
|
|
122
122
|
formatCompactNumber,
|
|
123
|
-
formatFileSize
|
|
123
|
+
formatFileSize,
|
|
124
|
+
formatDateTimeForDisplay,
|
|
125
|
+
formatDateOnlyForDisplay,
|
|
126
|
+
formatDateTimeForTable,
|
|
127
|
+
formatDateTimeForMap
|
|
124
128
|
} from './formatting/formatting';
|
|
129
|
+
export type { DateTimeFormatOptions } from './formatting/formatting';
|
|
125
130
|
|
|
126
131
|
// Organisation context utilities
|
|
127
132
|
export {
|
|
@@ -130,3 +135,24 @@ export {
|
|
|
130
135
|
getOrganisationContext,
|
|
131
136
|
isOrganisationContextAvailable
|
|
132
137
|
} from './context/organisationContext';
|
|
138
|
+
|
|
139
|
+
// Timezone utilities
|
|
140
|
+
export {
|
|
141
|
+
formatInTimeZone,
|
|
142
|
+
getTimezoneAbbreviation,
|
|
143
|
+
formatTimeInTimeZone,
|
|
144
|
+
getUserTimeZone,
|
|
145
|
+
toZonedTime,
|
|
146
|
+
fromZonedTime,
|
|
147
|
+
roundToNearestMinutes,
|
|
148
|
+
getTimeZoneDifference
|
|
149
|
+
} from './timezone';
|
|
150
|
+
|
|
151
|
+
// Location utilities
|
|
152
|
+
export {
|
|
153
|
+
formatCoordinates,
|
|
154
|
+
hasValidCoordinates,
|
|
155
|
+
areCoordinatesEqual,
|
|
156
|
+
getGoogleMapsUrl
|
|
157
|
+
} from './location';
|
|
158
|
+
export type { Coordinates } from './location';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Location Utilities Exports
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Location
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
formatCoordinates,
|
|
10
|
+
hasValidCoordinates,
|
|
11
|
+
areCoordinatesEqual,
|
|
12
|
+
getGoogleMapsUrl
|
|
13
|
+
} from './location';
|
|
14
|
+
|
|
15
|
+
export type { Coordinates } from './location';
|
|
16
|
+
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Location Utilities Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Location/__tests__
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for location utility functions.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and error handling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
formatCoordinates,
|
|
14
|
+
hasValidCoordinates,
|
|
15
|
+
areCoordinatesEqual,
|
|
16
|
+
getGoogleMapsUrl,
|
|
17
|
+
type Coordinates
|
|
18
|
+
} from './location';
|
|
19
|
+
|
|
20
|
+
describe('Location Utilities', () => {
|
|
21
|
+
describe('formatCoordinates', () => {
|
|
22
|
+
it('formats valid coordinates with 6 decimal places', () => {
|
|
23
|
+
const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
24
|
+
const result = formatCoordinates(coords);
|
|
25
|
+
expect(result).toBe('-37.813600, 144.963100');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('formats coordinates with fewer decimal places', () => {
|
|
29
|
+
const coords: Coordinates = { lat: 0, lng: 0 };
|
|
30
|
+
const result = formatCoordinates(coords);
|
|
31
|
+
expect(result).toBe('0.000000, 0.000000');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('formats coordinates with many decimal places', () => {
|
|
35
|
+
const coords: Coordinates = { lat: -37.813612345, lng: 144.963198765 };
|
|
36
|
+
const result = formatCoordinates(coords);
|
|
37
|
+
expect(result).toBe('-37.813612, 144.963199');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns "N/A" for undefined', () => {
|
|
41
|
+
const result = formatCoordinates(undefined);
|
|
42
|
+
expect(result).toBe('N/A');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns "N/A" for null', () => {
|
|
46
|
+
// @ts-expect-error - Testing null input
|
|
47
|
+
const result = formatCoordinates(null);
|
|
48
|
+
expect(result).toBe('N/A');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns "N/A" for missing lat', () => {
|
|
52
|
+
// @ts-expect-error - Testing invalid input
|
|
53
|
+
const result = formatCoordinates({ lng: 144.9631 });
|
|
54
|
+
expect(result).toBe('N/A');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns "N/A" for missing lng', () => {
|
|
58
|
+
// @ts-expect-error - Testing invalid input
|
|
59
|
+
const result = formatCoordinates({ lat: -37.8136 });
|
|
60
|
+
expect(result).toBe('N/A');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns "N/A" for NaN values', () => {
|
|
64
|
+
const coords: Coordinates = { lat: NaN, lng: 144.9631 };
|
|
65
|
+
const result = formatCoordinates(coords);
|
|
66
|
+
expect(result).toBe('N/A');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns "N/A" for Infinity values', () => {
|
|
70
|
+
const coords: Coordinates = { lat: Infinity, lng: 144.9631 };
|
|
71
|
+
const result = formatCoordinates(coords);
|
|
72
|
+
expect(result).toBe('N/A');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns "N/A" for non-number lat', () => {
|
|
76
|
+
// @ts-expect-error - Testing invalid input
|
|
77
|
+
const result = formatCoordinates({ lat: 'invalid', lng: 144.9631 });
|
|
78
|
+
expect(result).toBe('N/A');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns "N/A" for non-number lng', () => {
|
|
82
|
+
// @ts-expect-error - Testing invalid input
|
|
83
|
+
const result = formatCoordinates({ lat: -37.8136, lng: 'invalid' });
|
|
84
|
+
expect(result).toBe('N/A');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('hasValidCoordinates', () => {
|
|
89
|
+
it('returns true for valid coordinates', () => {
|
|
90
|
+
const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
91
|
+
expect(hasValidCoordinates(coords)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns true for coordinates at boundaries', () => {
|
|
95
|
+
expect(hasValidCoordinates({ lat: -90, lng: -180 })).toBe(true);
|
|
96
|
+
expect(hasValidCoordinates({ lat: 90, lng: 180 })).toBe(true);
|
|
97
|
+
expect(hasValidCoordinates({ lat: 0, lng: 0 })).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns false for lat out of range (too low)', () => {
|
|
101
|
+
expect(hasValidCoordinates({ lat: -91, lng: 0 })).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns false for lat out of range (too high)', () => {
|
|
105
|
+
expect(hasValidCoordinates({ lat: 91, lng: 0 })).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns false for lng out of range (too low)', () => {
|
|
109
|
+
expect(hasValidCoordinates({ lat: 0, lng: -181 })).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns false for lng out of range (too high)', () => {
|
|
113
|
+
expect(hasValidCoordinates({ lat: 0, lng: 181 })).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns false for undefined', () => {
|
|
117
|
+
expect(hasValidCoordinates(undefined)).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns false for null', () => {
|
|
121
|
+
// @ts-expect-error - Testing null input
|
|
122
|
+
expect(hasValidCoordinates(null)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns false for missing lat', () => {
|
|
126
|
+
expect(hasValidCoordinates({ lng: 144.9631 })).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('returns false for missing lng', () => {
|
|
130
|
+
expect(hasValidCoordinates({ lat: -37.8136 })).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns false for NaN lat', () => {
|
|
134
|
+
expect(hasValidCoordinates({ lat: NaN, lng: 144.9631 })).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('returns false for NaN lng', () => {
|
|
138
|
+
expect(hasValidCoordinates({ lat: -37.8136, lng: NaN })).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns false for Infinity lat', () => {
|
|
142
|
+
expect(hasValidCoordinates({ lat: Infinity, lng: 144.9631 })).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns false for Infinity lng', () => {
|
|
146
|
+
expect(hasValidCoordinates({ lat: -37.8136, lng: Infinity })).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns false for non-number lat', () => {
|
|
150
|
+
// @ts-expect-error - Testing invalid input
|
|
151
|
+
expect(hasValidCoordinates({ lat: 'invalid', lng: 144.9631 })).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns false for non-number lng', () => {
|
|
155
|
+
// @ts-expect-error - Testing invalid input
|
|
156
|
+
expect(hasValidCoordinates({ lat: -37.8136, lng: 'invalid' })).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('areCoordinatesEqual', () => {
|
|
161
|
+
it('returns true for identical coordinates', () => {
|
|
162
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
163
|
+
const coords2: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
164
|
+
expect(areCoordinatesEqual(coords1, coords2)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns true for coordinates within default tolerance', () => {
|
|
168
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
169
|
+
const coords2: Coordinates = { lat: -37.8137, lng: 144.9632 };
|
|
170
|
+
expect(areCoordinatesEqual(coords1, coords2)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns false for coordinates outside default tolerance', () => {
|
|
174
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
175
|
+
const coords2: Coordinates = { lat: -37.8150, lng: 144.9650 };
|
|
176
|
+
expect(areCoordinatesEqual(coords1, coords2)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns true for coordinates within custom tolerance', () => {
|
|
180
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
181
|
+
const coords2: Coordinates = { lat: -37.8137, lng: 144.9632 };
|
|
182
|
+
expect(areCoordinatesEqual(coords1, coords2, 0.001)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('returns false for coordinates outside custom tolerance', () => {
|
|
186
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
187
|
+
const coords2: Coordinates = { lat: -37.8137, lng: 144.9632 };
|
|
188
|
+
expect(areCoordinatesEqual(coords1, coords2, 0.00001)).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns true when both are null', () => {
|
|
192
|
+
expect(areCoordinatesEqual(null, null)).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns true when both are undefined', () => {
|
|
196
|
+
expect(areCoordinatesEqual(undefined, undefined)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns false when one is null and other is valid', () => {
|
|
200
|
+
const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
201
|
+
expect(areCoordinatesEqual(null, coords)).toBe(false);
|
|
202
|
+
expect(areCoordinatesEqual(coords, null)).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('returns false when one is undefined and other is valid', () => {
|
|
206
|
+
const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
207
|
+
expect(areCoordinatesEqual(undefined, coords)).toBe(false);
|
|
208
|
+
expect(areCoordinatesEqual(coords, undefined)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns false for invalid coordinates', () => {
|
|
212
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
213
|
+
// @ts-expect-error - Testing invalid input
|
|
214
|
+
const coords2 = { lat: 91, lng: 0 };
|
|
215
|
+
expect(areCoordinatesEqual(coords1, coords2)).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('handles edge case: exactly at tolerance boundary', () => {
|
|
219
|
+
const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
220
|
+
const coords2: Coordinates = { lat: -37.8136 + 0.0001, lng: 144.9631 };
|
|
221
|
+
expect(areCoordinatesEqual(coords1, coords2, 0.0001)).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('getGoogleMapsUrl', () => {
|
|
226
|
+
it('generates URL for valid coordinates', () => {
|
|
227
|
+
const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
|
|
228
|
+
const result = getGoogleMapsUrl(coords);
|
|
229
|
+
expect(result).toBe('https://www.google.com/maps/search/?api=1&query=-37.8136,144.9631');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('generates URL with negative coordinates', () => {
|
|
233
|
+
const coords: Coordinates = { lat: -90, lng: -180 };
|
|
234
|
+
const result = getGoogleMapsUrl(coords);
|
|
235
|
+
expect(result).toBe('https://www.google.com/maps/search/?api=1&query=-90,-180');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('generates URL with positive coordinates', () => {
|
|
239
|
+
const coords: Coordinates = { lat: 90, lng: 180 };
|
|
240
|
+
const result = getGoogleMapsUrl(coords);
|
|
241
|
+
expect(result).toBe('https://www.google.com/maps/search/?api=1&query=90,180');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('returns empty string for undefined', () => {
|
|
245
|
+
const result = getGoogleMapsUrl(undefined);
|
|
246
|
+
expect(result).toBe('');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('returns empty string for null', () => {
|
|
250
|
+
// @ts-expect-error - Testing null input
|
|
251
|
+
const result = getGoogleMapsUrl(null);
|
|
252
|
+
expect(result).toBe('');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('returns empty string for invalid coordinates', () => {
|
|
256
|
+
// @ts-expect-error - Testing invalid input
|
|
257
|
+
const result = getGoogleMapsUrl({ lat: 91, lng: 0 });
|
|
258
|
+
expect(result).toBe('');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns empty string for missing lat', () => {
|
|
262
|
+
// @ts-expect-error - Testing invalid input
|
|
263
|
+
const result = getGoogleMapsUrl({ lng: 144.9631 });
|
|
264
|
+
expect(result).toBe('');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('returns empty string for missing lng', () => {
|
|
268
|
+
// @ts-expect-error - Testing invalid input
|
|
269
|
+
const result = getGoogleMapsUrl({ lat: -37.8136 });
|
|
270
|
+
expect(result).toBe('');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('returns empty string for NaN values', () => {
|
|
274
|
+
const coords: Coordinates = { lat: NaN, lng: 144.9631 };
|
|
275
|
+
const result = getGoogleMapsUrl(coords);
|
|
276
|
+
expect(result).toBe('');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns empty string for Infinity values', () => {
|
|
280
|
+
const coords: Coordinates = { lat: Infinity, lng: 144.9631 };
|
|
281
|
+
const result = getGoogleMapsUrl(coords);
|
|
282
|
+
expect(result).toBe('');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Location Utilities
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Location
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Utility functions for working with geographic coordinates.
|
|
8
|
+
* Provides functions for formatting, validating, comparing, and generating URLs for coordinates.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Format coordinates for display
|
|
12
|
+
* - Validate coordinate objects
|
|
13
|
+
* - Compare coordinates with tolerance
|
|
14
|
+
* - Generate Google Maps URLs
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { formatCoordinates, hasValidCoordinates, areCoordinatesEqual, getGoogleMapsUrl } from '@jmruthers/pace-core/utils/location';
|
|
19
|
+
*
|
|
20
|
+
* const coords = { lat: -37.8136, lng: 144.9631 };
|
|
21
|
+
*
|
|
22
|
+
* // Format for display
|
|
23
|
+
* formatCoordinates(coords); // "-37.813600, 144.963100"
|
|
24
|
+
*
|
|
25
|
+
* // Validate
|
|
26
|
+
* hasValidCoordinates(coords); // true
|
|
27
|
+
*
|
|
28
|
+
* // Compare
|
|
29
|
+
* areCoordinatesEqual(coords, { lat: -37.8137, lng: 144.9632 }); // true (within tolerance)
|
|
30
|
+
*
|
|
31
|
+
* // Generate Google Maps URL
|
|
32
|
+
* getGoogleMapsUrl(coords); // "https://www.google.com/maps/search/?api=1&query=-37.8136,144.9631"
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Coordinate interface for latitude and longitude
|
|
38
|
+
*/
|
|
39
|
+
export interface Coordinates {
|
|
40
|
+
lat: number;
|
|
41
|
+
lng: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format coordinates as a string with 6 decimal places
|
|
46
|
+
*
|
|
47
|
+
* @param coords - Coordinate object with lat and lng
|
|
48
|
+
* @returns Formatted string "lat, lng" or "N/A" if invalid
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* formatCoordinates({ lat: -37.8136, lng: 144.9631 });
|
|
53
|
+
* // "-37.813600, 144.963100"
|
|
54
|
+
*
|
|
55
|
+
* formatCoordinates(undefined);
|
|
56
|
+
* // "N/A"
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function formatCoordinates(coords?: Coordinates): string {
|
|
60
|
+
if (!coords || typeof coords.lat !== 'number' || typeof coords.lng !== 'number') {
|
|
61
|
+
return 'N/A';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isFinite(coords.lat) || !isFinite(coords.lng)) {
|
|
65
|
+
return 'N/A';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if coordinates are valid
|
|
73
|
+
*
|
|
74
|
+
* @param coords - Coordinate object to validate
|
|
75
|
+
* @returns true if coordinates are valid, false otherwise
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* hasValidCoordinates({ lat: -37.8136, lng: 144.9631 }); // true
|
|
80
|
+
* hasValidCoordinates({ lat: 91, lng: 0 }); // false (lat out of range)
|
|
81
|
+
* hasValidCoordinates(undefined); // false
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function hasValidCoordinates(coords?: { lat?: number; lng?: number }): boolean {
|
|
85
|
+
if (!coords) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { lat, lng } = coords;
|
|
90
|
+
|
|
91
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isFinite(lat) || !isFinite(lng)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Latitude must be between -90 and 90
|
|
100
|
+
if (lat < -90 || lat > 90) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Longitude must be between -180 and 180
|
|
105
|
+
if (lng < -180 || lng > 180) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if two coordinates are equal within a tolerance
|
|
114
|
+
*
|
|
115
|
+
* @param coords1 - First coordinate object
|
|
116
|
+
* @param coords2 - Second coordinate object
|
|
117
|
+
* @param tolerance - Tolerance in degrees (default: 0.0001° ≈ 11 meters)
|
|
118
|
+
* @returns true if coordinates are within tolerance, false otherwise
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const coords1 = { lat: -37.8136, lng: 144.9631 };
|
|
123
|
+
* const coords2 = { lat: -37.8137, lng: 144.9632 };
|
|
124
|
+
* areCoordinatesEqual(coords1, coords2); // true (within default tolerance)
|
|
125
|
+
* areCoordinatesEqual(coords1, coords2, 0.00001); // false (stricter tolerance)
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function areCoordinatesEqual(
|
|
129
|
+
coords1: Coordinates | null | undefined,
|
|
130
|
+
coords2: Coordinates | null | undefined,
|
|
131
|
+
tolerance: number = 0.0001
|
|
132
|
+
): boolean {
|
|
133
|
+
// Both null/undefined are considered equal
|
|
134
|
+
if (!coords1 && !coords2) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// One null/undefined and one not are not equal
|
|
139
|
+
if (!coords1 || !coords2) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validate both coordinates
|
|
144
|
+
if (!hasValidCoordinates(coords1) || !hasValidCoordinates(coords2)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if within tolerance (with small epsilon for floating point precision)
|
|
149
|
+
const epsilon = 1e-10;
|
|
150
|
+
const latDiff = Math.abs(coords1.lat - coords2.lat);
|
|
151
|
+
const lngDiff = Math.abs(coords1.lng - coords2.lng);
|
|
152
|
+
|
|
153
|
+
return latDiff <= tolerance + epsilon && lngDiff <= tolerance + epsilon;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate a Google Maps search URL for coordinates
|
|
158
|
+
*
|
|
159
|
+
* @param coords - Coordinate object with lat and lng
|
|
160
|
+
* @returns Google Maps search URL or empty string if invalid
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* getGoogleMapsUrl({ lat: -37.8136, lng: 144.9631 });
|
|
165
|
+
* // "https://www.google.com/maps/search/?api=1&query=-37.8136,144.9631"
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function getGoogleMapsUrl(coords?: Coordinates): string {
|
|
169
|
+
if (!coords || !hasValidCoordinates(coords)) {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return `https://www.google.com/maps/search/?api=1&query=${coords.lat},${coords.lng}`;
|
|
174
|
+
}
|
|
175
|
+
|