@trackany-device/components 1.0.0
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/package.json +185 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/map/arrows/map-arrow-blue.png +0 -0
- package/src/assets/map/arrows/map-arrow-green.png +0 -0
- package/src/assets/map/arrows/map-arrow-purple.png +0 -0
- package/src/assets/map/arrows/map-arrow-red.png +0 -0
- package/src/assets/map/flags/flag-blue.png +0 -0
- package/src/assets/map/flags/flag-green.png +0 -0
- package/src/assets/map/flags/flag-red.png +0 -0
- package/src/assets/map/flags/flag-yellow.png +0 -0
- package/src/assets/map/pins/map-pin-blue.png +0 -0
- package/src/assets/map/pins/map-pin-green.png +0 -0
- package/src/assets/map/pins/map-pin-purple.png +0 -0
- package/src/assets/map/pins/map-pin-red.png +0 -0
- package/src/components/Card.tsx +9 -0
- package/src/components/alert-error.tsx +24 -0
- package/src/components/app-content.tsx +22 -0
- package/src/components/app-header.tsx +153 -0
- package/src/components/app-logo-icon.tsx +13 -0
- package/src/components/app-logo.tsx +21 -0
- package/src/components/app-shell.tsx +19 -0
- package/src/components/app-sidebar-header.tsx +68 -0
- package/src/components/app-sidebar.tsx +106 -0
- package/src/components/appearance-tabs.tsx +46 -0
- package/src/components/breadcrumbs.tsx +50 -0
- package/src/components/cms/blurred-image.tsx +111 -0
- package/src/components/cms/section-bg.tsx +473 -0
- package/src/components/cms/section-button.tsx +127 -0
- package/src/components/cms/sections/banner-5050-section.tsx +135 -0
- package/src/components/cms/sections/blogs-listing-section.tsx +270 -0
- package/src/components/cms/sections/cards-grid-section.tsx +185 -0
- package/src/components/cms/sections/contact-form-section.tsx +157 -0
- package/src/components/cms/sections/cta-section.tsx +101 -0
- package/src/components/cms/sections/featured-blog-slider-section.tsx +256 -0
- package/src/components/cms/sections/featured-products-grid-section.tsx +173 -0
- package/src/components/cms/sections/featured-solutions-grid-section.tsx +183 -0
- package/src/components/cms/sections/hero-section.tsx +180 -0
- package/src/components/cms/sections/solutions-with-filter-section.tsx +234 -0
- package/src/components/cms/sections/text-section.tsx +77 -0
- package/src/components/cutout-image.tsx +228 -0
- package/src/components/devices/devices-mini-map.tsx +275 -0
- package/src/components/docs/docs-shell.tsx +280 -0
- package/src/components/fleet-hero-animated.tsx +383 -0
- package/src/components/input-error.tsx +17 -0
- package/src/components/keenicons/assets/duotone/Read Me.txt +7 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/duotone/demo.html +12424 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.svg +1109 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.ttf +0 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.woff +0 -0
- package/src/components/keenicons/assets/duotone/selection.json +17313 -0
- package/src/components/keenicons/assets/duotone/style.css +4931 -0
- package/src/components/keenicons/assets/filled/Read Me.txt +7 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/filled/demo.html +12370 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.svg +1082 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.ttf +0 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.woff +0 -0
- package/src/components/keenicons/assets/filled/selection.json +17096 -0
- package/src/components/keenicons/assets/filled/style.css +4769 -0
- package/src/components/keenicons/assets/outline/Read Me.txt +7 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/outline/demo.html +11356 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.svg +575 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.ttf +0 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.woff +0 -0
- package/src/components/keenicons/assets/outline/selection.json +13054 -0
- package/src/components/keenicons/assets/outline/style.css +1721 -0
- package/src/components/keenicons/assets/solid/Read Me.txt +7 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/solid/demo.html +11356 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.svg +575 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.ttf +0 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.woff +0 -0
- package/src/components/keenicons/assets/solid/selection.json +13048 -0
- package/src/components/keenicons/assets/solid/style.css +1721 -0
- package/src/components/keenicons/assets/styles.css +4 -0
- package/src/components/keenicons/index.ts +2 -0
- package/src/components/keenicons/keenicons.tsx +16 -0
- package/src/components/keenicons/types.ts +7 -0
- package/src/components/nav-footer.tsx +49 -0
- package/src/components/nav-main.tsx +53 -0
- package/src/components/nav-user.tsx +59 -0
- package/src/components/notification-bell.tsx +190 -0
- package/src/components/products/product-card.tsx +159 -0
- package/src/components/text-link.tsx +23 -0
- package/src/components/ui/accordion-menu.tsx +322 -0
- package/src/components/ui/accordion.tsx +133 -0
- package/src/components/ui/alert-dialog.tsx +82 -0
- package/src/components/ui/alert.tsx +63 -0
- package/src/components/ui/avatar-group.tsx +129 -0
- package/src/components/ui/avatar.tsx +67 -0
- package/src/components/ui/badge.tsx +230 -0
- package/src/components/ui/breadcrumb.tsx +88 -0
- package/src/components/ui/button.tsx +412 -0
- package/src/components/ui/calendar.tsx +56 -0
- package/src/components/ui/card.tsx +147 -0
- package/src/components/ui/chart.tsx +290 -0
- package/src/components/ui/checkbox.tsx +47 -0
- package/src/components/ui/code.tsx +45 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command-palette.tsx +189 -0
- package/src/components/ui/command.tsx +138 -0
- package/src/components/ui/cookie-banner.tsx +220 -0
- package/src/components/ui/copy-button.tsx +60 -0
- package/src/components/ui/data-grid-column-filter.tsx +124 -0
- package/src/components/ui/data-grid-column-header.tsx +284 -0
- package/src/components/ui/data-grid-column-visibility.tsx +38 -0
- package/src/components/ui/data-grid-pagination.tsx +206 -0
- package/src/components/ui/data-grid-table-dnd-rows.tsx +147 -0
- package/src/components/ui/data-grid-table-dnd.tsx +175 -0
- package/src/components/ui/data-grid-table.tsx +500 -0
- package/src/components/ui/data-grid.tsx +193 -0
- package/src/components/ui/data-list.tsx +76 -0
- package/src/components/ui/datefield.tsx +91 -0
- package/src/components/ui/dialog.tsx +139 -0
- package/src/components/ui/divider.tsx +41 -0
- package/src/components/ui/drawer.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +224 -0
- package/src/components/ui/empty-state.tsx +54 -0
- package/src/components/ui/file-upload.tsx +152 -0
- package/src/components/ui/form.tsx +88 -0
- package/src/components/ui/icon.tsx +14 -0
- package/src/components/ui/input-otp.tsx +71 -0
- package/src/components/ui/input.tsx +155 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +31 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +37 -0
- package/src/components/ui/placeholder-pattern.tsx +21 -0
- package/src/components/ui/popover.tsx +50 -0
- package/src/components/ui/progress.tsx +65 -0
- package/src/components/ui/radio-group.tsx +73 -0
- package/src/components/ui/resizable.tsx +39 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +234 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +721 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/sonner.tsx +28 -0
- package/src/components/ui/sortable.tsx +724 -0
- package/src/components/ui/spinner.tsx +17 -0
- package/src/components/ui/stat-card.tsx +82 -0
- package/src/components/ui/stepper.tsx +410 -0
- package/src/components/ui/switch.tsx +68 -0
- package/src/components/ui/table.tsx +42 -0
- package/src/components/ui/tabs.tsx +196 -0
- package/src/components/ui/timeline.tsx +90 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/user-info.tsx +33 -0
- package/src/components/user-menu-content.tsx +53 -0
- package/src/components/web/SiteFooter.tsx +154 -0
- package/src/components/web/SiteHeader.tsx +159 -0
- package/src/components/workflows/workflow-canvas.tsx +321 -0
- package/src/controls/Blockquote.tsx +25 -0
- package/src/controls/Button.tsx +101 -0
- package/src/controls/Checkbox.tsx +29 -0
- package/src/controls/DateField.tsx +37 -0
- package/src/controls/FormField.tsx +20 -0
- package/src/controls/Heading.tsx +28 -0
- package/src/controls/Input.tsx +21 -0
- package/src/controls/Label.tsx +18 -0
- package/src/controls/Paragraph.tsx +39 -0
- package/src/controls/PasswordInput.tsx +40 -0
- package/src/controls/RadioGroup.tsx +70 -0
- package/src/controls/Select.tsx +24 -0
- package/src/controls/Slider.tsx +33 -0
- package/src/controls/Switch.tsx +31 -0
- package/src/controls/Textarea.tsx +22 -0
- package/src/elements/ConfirmPasswordForm.tsx +43 -0
- package/src/elements/DeviceStatusBadge.tsx +38 -0
- package/src/elements/DriverCard.tsx +67 -0
- package/src/elements/ForgotPasswordForm.tsx +64 -0
- package/src/elements/IncidentCard.tsx +67 -0
- package/src/elements/LoginForm.tsx +100 -0
- package/src/elements/OtpForm.tsx +71 -0
- package/src/elements/RegisterForm.tsx +150 -0
- package/src/elements/ResetPasswordForm.tsx +72 -0
- package/src/elements/SmsChallengeForm.tsx +104 -0
- package/src/elements/VehicleCard.tsx +73 -0
- package/src/elements/VerifyEmailForm.tsx +39 -0
- package/src/hooks/use-appearance.tsx +117 -0
- package/src/hooks/use-applied-theme.ts +98 -0
- package/src/hooks/use-clipboard.ts +34 -0
- package/src/hooks/use-current-url.ts +83 -0
- package/src/hooks/use-dark-mode.ts +48 -0
- package/src/hooks/use-flash-toast.ts +29 -0
- package/src/hooks/use-initials.tsx +24 -0
- package/src/hooks/use-mobile-navigation.ts +12 -0
- package/src/hooks/use-mobile.tsx +38 -0
- package/src/index.ts +408 -0
- package/src/layouts/AppLayout.tsx +60 -0
- package/src/layouts/AuthLayout.tsx +32 -0
- package/src/layouts/SettingsLayout.tsx +21 -0
- package/src/layouts/app/AIChatLayout.tsx +73 -0
- package/src/layouts/app/AsideSidebarLayout.tsx +3 -0
- package/src/layouts/app/CalendarSidebarLayout.tsx +69 -0
- package/src/layouts/app/CommunitiesNavbarLayout.tsx +3 -0
- package/src/layouts/app/DualNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/FocusSidebarLayout.tsx +75 -0
- package/src/layouts/app/MailLayout.tsx +69 -0
- package/src/layouts/app/MegaMenuHeaderLayout.tsx +3 -0
- package/src/layouts/app/MegaMenuLayout.tsx +81 -0
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +88 -0
- package/src/layouts/app/MegaMenuSearchNavbarLayout.tsx +3 -0
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +88 -0
- package/src/layouts/app/NavbarCollapsibleLinksLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarLayout.tsx +92 -0
- package/src/layouts/app/NavbarSimpleSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarTitledSidebarLayout.tsx +3 -0
- package/src/layouts/app/PanelSidebarLayout.tsx +3 -0
- package/src/layouts/app/SearchNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/SidebarBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/SidebarCleanLayout.tsx +3 -0
- package/src/layouts/app/SidebarCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/SidebarContentLayout.tsx +3 -0
- package/src/layouts/app/SidebarDualMenuLayout.tsx +104 -0
- package/src/layouts/app/SidebarFixedLayout.tsx +166 -0
- package/src/layouts/app/SidebarFooterNavbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarHeaderMenuLayout.tsx +3 -0
- package/src/layouts/app/SidebarMegaMenuLayout.tsx +4 -0
- package/src/layouts/app/SidebarMinimalLayout.tsx +70 -0
- package/src/layouts/app/SidebarMobileSearchLayout.tsx +3 -0
- package/src/layouts/app/SidebarMultiPanelLayout.tsx +3 -0
- package/src/layouts/app/SidebarPrimarySecondaryLayout.tsx +3 -0
- package/src/layouts/app/SidebarSearchHeaderLayout.tsx +103 -0
- package/src/layouts/app/SidebarSearchToolbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsDualLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsLayout.tsx +98 -0
- package/src/layouts/app/SidebarTreeLayout.tsx +3 -0
- package/src/layouts/app/SplitNavbarLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarLayout.tsx +99 -0
- package/src/layouts/app/TopNavLayout.tsx +105 -0
- package/src/layouts/app/TopNavLinksLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceNavbarLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +98 -0
- package/src/layouts/app/WorkspaceSidebarTitleLayout.tsx +3 -0
- package/src/layouts/app/app-header-layout.tsx +45 -0
- package/src/layouts/app/app-sidebar-layout.tsx +56 -0
- package/src/layouts/app/layout-context.tsx +44 -0
- package/src/layouts/app/layout-types.ts +47 -0
- package/src/layouts/app/partials/Footer.tsx +35 -0
- package/src/layouts/app/partials/HeaderTopbar.tsx +96 -0
- package/src/layouts/app/partials/Navbar.tsx +85 -0
- package/src/layouts/app/partials/Toolbar.tsx +47 -0
- package/src/layouts/app-layout.tsx +29 -0
- package/src/layouts/auth/AuthBrandedLayout.tsx +58 -0
- package/src/layouts/auth/AuthCardLayout.tsx +31 -0
- package/src/layouts/auth/AuthCenteredLayout.tsx +41 -0
- package/src/layouts/auth/AuthClassicLayout.tsx +41 -0
- package/src/layouts/auth/AuthSimpleLayout.tsx +33 -0
- package/src/layouts/auth/AuthSplitLayout.tsx +89 -0
- package/src/layouts/web-app-layout.tsx +162 -0
- package/src/layouts/web-layout.tsx +23 -0
- package/src/lib/datetime.ts +188 -0
- package/src/lib/google-maps-loader.ts +99 -0
- package/src/lib/location.ts +127 -0
- package/src/lib/lucide-icon-map.ts +132 -0
- package/src/lib/map-markers.ts +124 -0
- package/src/lib/map-styles.ts +351 -0
- package/src/lib/utils.ts +11 -0
- package/src/platform/adapters/default.tsx +156 -0
- package/src/platform/adapters/inertia.tsx +88 -0
- package/src/platform/adapters/nextjs.ts +86 -0
- package/src/platform/context.tsx +106 -0
- package/src/platform/index.ts +27 -0
- package/src/platform/types.ts +105 -0
- package/src/styles/layouts/sidebar-fixed.css +161 -0
- package/src/styles/themes.css +583 -0
- package/src/types/assets.d.ts +5 -0
- package/src/types/auth.ts +25 -0
- package/src/types/global.d.ts +13 -0
- package/src/types/index.ts +9 -0
- package/src/types/navigation.ts +15 -0
- package/src/types/ui.ts +32 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { usePlatformPageProps } from '../platform/context';
|
|
2
|
+
/**
|
|
3
|
+
* Datetime utilities for rendering server-UTC timestamps in the user's local
|
|
4
|
+
* timezone. The server emits ISO-8601 with a 'Z' suffix (see Carbon serialiser
|
|
5
|
+
* configured in AppServiceProvider). All helpers in this module accept either
|
|
6
|
+
* an ISO string or a Date / null and return display strings.
|
|
7
|
+
*
|
|
8
|
+
* Display timezone resolution order:
|
|
9
|
+
* 1. explicit `timezone` argument
|
|
10
|
+
* 2. usePage().props.user_timezone (Inertia shared prop) — resolved via useUserTimezone()
|
|
11
|
+
* 3. Intl.DateTimeFormat().resolvedOptions().timeZone (browser default)
|
|
12
|
+
* 4. 'UTC' as final fallback
|
|
13
|
+
*
|
|
14
|
+
* NOTE: useUserTimezone() and reportBrowserTimezone() import from @inertiajs/react
|
|
15
|
+
* which must be available in the consuming app.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
type IsoOrDate = string | Date | null | undefined;
|
|
20
|
+
|
|
21
|
+
const isoCache = new Map<string, Date>();
|
|
22
|
+
|
|
23
|
+
function toDate(value: IsoOrDate): Date | null {
|
|
24
|
+
if (value === null || value === undefined) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (value instanceof Date) {
|
|
29
|
+
return isNaN(value.getTime()) ? null : value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof value !== 'string') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cached = isoCache.get(value);
|
|
37
|
+
|
|
38
|
+
if (cached !== undefined) {
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const d = new Date(value);
|
|
43
|
+
|
|
44
|
+
if (isNaN(d.getTime())) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
isoCache.set(value, d);
|
|
49
|
+
|
|
50
|
+
return d;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function browserTimezone(): string {
|
|
54
|
+
try {
|
|
55
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
56
|
+
} catch {
|
|
57
|
+
return 'UTC';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let _sharedTimezone: string | null = null;
|
|
62
|
+
|
|
63
|
+
export function setSharedTimezone(tz: string | null): void {
|
|
64
|
+
_sharedTimezone = tz;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveTimezone(explicit?: string | null): string {
|
|
68
|
+
if (explicit) {
|
|
69
|
+
return explicit;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (_sharedTimezone) {
|
|
73
|
+
return _sharedTimezone;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return browserTimezone();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const DEFAULT_OPTIONS: Intl.DateTimeFormatOptions = {
|
|
80
|
+
year: 'numeric',
|
|
81
|
+
month: 'short',
|
|
82
|
+
day: 'numeric',
|
|
83
|
+
hour: '2-digit',
|
|
84
|
+
minute: '2-digit',
|
|
85
|
+
hour12: false,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function formatLocalDateTime(
|
|
89
|
+
value: IsoOrDate,
|
|
90
|
+
options: Intl.DateTimeFormatOptions = DEFAULT_OPTIONS,
|
|
91
|
+
timezone?: string,
|
|
92
|
+
): string {
|
|
93
|
+
const d = toDate(value);
|
|
94
|
+
|
|
95
|
+
if (d === null) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
100
|
+
...options,
|
|
101
|
+
timeZone: resolveTimezone(timezone),
|
|
102
|
+
}).format(d);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatLocalDate(value: IsoOrDate, timezone?: string): string {
|
|
106
|
+
return formatLocalDateTime(
|
|
107
|
+
value,
|
|
108
|
+
{ year: 'numeric', month: 'short', day: 'numeric' },
|
|
109
|
+
timezone,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatLocalTime(value: IsoOrDate, timezone?: string): string {
|
|
114
|
+
return formatLocalDateTime(
|
|
115
|
+
value,
|
|
116
|
+
{
|
|
117
|
+
hour: '2-digit',
|
|
118
|
+
minute: '2-digit',
|
|
119
|
+
second: '2-digit',
|
|
120
|
+
hour12: false,
|
|
121
|
+
},
|
|
122
|
+
timezone,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatRelative(value: IsoOrDate): string {
|
|
127
|
+
const d = toDate(value);
|
|
128
|
+
|
|
129
|
+
if (d === null) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const diffMs = Date.now() - d.getTime();
|
|
134
|
+
const sec = Math.round(diffMs / 1000);
|
|
135
|
+
|
|
136
|
+
if (Math.abs(sec) < 45) {
|
|
137
|
+
return 'just now';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const min = Math.round(sec / 60);
|
|
141
|
+
|
|
142
|
+
if (Math.abs(min) < 60) {
|
|
143
|
+
return `${min}m ago`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hr = Math.round(min / 60);
|
|
147
|
+
|
|
148
|
+
if (Math.abs(hr) < 24) {
|
|
149
|
+
return `${hr}h ago`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const days = Math.round(hr / 24);
|
|
153
|
+
|
|
154
|
+
if (Math.abs(days) < 30) {
|
|
155
|
+
return `${days}d ago`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return formatLocalDate(d);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns the user's timezone resolved from Inertia shared props.
|
|
163
|
+
* Use inside React components; outside components prefer resolveTimezone().
|
|
164
|
+
*/
|
|
165
|
+
export function useUserTimezone(): string {
|
|
166
|
+
const { user_timezone } = usePlatformPageProps<{ user_timezone?: string | null }>();
|
|
167
|
+
return resolveTimezone(user_timezone);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Send the browser's resolved timezone to the server so unauthenticated
|
|
172
|
+
* users (and the X-Browser-Timezone fallback in HandleInertiaRequests) get
|
|
173
|
+
* a sensible default. Idempotent — safe to call on every page load.
|
|
174
|
+
*/
|
|
175
|
+
interface TimezoneRouter { on: (e: string, cb: (event: unknown) => void) => () => void; }
|
|
176
|
+
|
|
177
|
+
export function reportBrowserTimezone(router?: TimezoneRouter): void {
|
|
178
|
+
if (typeof window === 'undefined') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const tz = browserTimezone();
|
|
183
|
+
if (!router) return;
|
|
184
|
+
router.on('before', (event: unknown) => {
|
|
185
|
+
const xhr = (event as { detail: { visit?: { headers?: Record<string, string> } } }).detail.visit?.headers ?? {};
|
|
186
|
+
(xhr as Record<string, string>)['X-Browser-Timezone'] = tz;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { setOptions, importLibrary } from '@googlemaps/js-api-loader';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared Google Maps JS API loader. All map-rendering surfaces
|
|
5
|
+
* (`/map`, `/playback`, `/incidents/{id}`, `/beats/create+edit`,
|
|
6
|
+
* `LocationPlayback`, hero `kind:map` section bg) call `loadGoogleMaps()`
|
|
7
|
+
* which returns the same memoised `Promise<typeof google.maps>`.
|
|
8
|
+
*
|
|
9
|
+
* Uses the v2 functional API (`setOptions` + `importLibrary`) — the
|
|
10
|
+
* legacy `Loader` class is deprecated. setOptions is idempotent and
|
|
11
|
+
* importLibrary triggers the actual script injection on first call.
|
|
12
|
+
*
|
|
13
|
+
* SECURITY: the API key ships in the JS bundle. Restrict it in the
|
|
14
|
+
* Google Cloud Console by HTTP referrer to the platform's central +
|
|
15
|
+
* tenant subdomains so a leaked key can't be abused.
|
|
16
|
+
*
|
|
17
|
+
* COST: every page-view that mounts a map fires one Google Maps map
|
|
18
|
+
* load (billable at ~$7 per 1,000 loads under the Dynamic Maps SKU).
|
|
19
|
+
* Pages that mount a map should gate on `hasGoogleMapsKey()` so dev /
|
|
20
|
+
* test / CI environments without a key render an empty placeholder
|
|
21
|
+
* instead of triggering loader errors.
|
|
22
|
+
*
|
|
23
|
+
* NOTE: Reads from import.meta.env.VITE_GOOGLE_MAPS_API_KEY and
|
|
24
|
+
* import.meta.env.VITE_GOOGLE_MAPS_LIBRARIES — configure these env
|
|
25
|
+
* vars in the consuming app's Vite config.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// import.meta.env is only available in Vite bundles (not Next.js/webpack).
|
|
29
|
+
// Read lazily so module evaluation doesn't throw in non-Vite environments.
|
|
30
|
+
function getEnv(key: string): string {
|
|
31
|
+
try {
|
|
32
|
+
return (import.meta as unknown as { env?: Record<string, string> }).env?.[key] ?? '';
|
|
33
|
+
} catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const KEY: string = getEnv('VITE_GOOGLE_MAPS_API_KEY');
|
|
39
|
+
const EXTRA_LIBS: string = getEnv('VITE_GOOGLE_MAPS_LIBRARIES');
|
|
40
|
+
|
|
41
|
+
// Libraries we always load:
|
|
42
|
+
// - places → Autocomplete for beat editor address search
|
|
43
|
+
// - drawing → DrawingManager + editable Polygon for beat polygon edits
|
|
44
|
+
const BASE_LIBRARIES = ['places', 'drawing'];
|
|
45
|
+
|
|
46
|
+
let initialised = false;
|
|
47
|
+
let loaderPromise: Promise<typeof google.maps> | null = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns true when a Google Maps API key is configured. Pages should
|
|
51
|
+
* gate map rendering on this to avoid loader errors in test / CI / dev
|
|
52
|
+
* environments without a key.
|
|
53
|
+
*/
|
|
54
|
+
export function hasGoogleMapsKey(): boolean {
|
|
55
|
+
return KEY.length > 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Lazy-load the Google Maps JS API. Memoised — every caller after the
|
|
60
|
+
* first await gets the cached resolution.
|
|
61
|
+
*
|
|
62
|
+
* Throws if no key is configured; callers should check `hasGoogleMapsKey()`
|
|
63
|
+
* first and render a fallback when it's false.
|
|
64
|
+
*/
|
|
65
|
+
export function loadGoogleMaps(): Promise<typeof google.maps> {
|
|
66
|
+
if (!KEY) {
|
|
67
|
+
return Promise.reject(
|
|
68
|
+
new Error(
|
|
69
|
+
'VITE_GOOGLE_MAPS_API_KEY is not set. Maps will not load.',
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (loaderPromise) {
|
|
75
|
+
return loaderPromise;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!initialised) {
|
|
79
|
+
const extra = EXTRA_LIBS.split(',')
|
|
80
|
+
.map((s) => s.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
const libraries = Array.from(new Set([...BASE_LIBRARIES, ...extra]));
|
|
83
|
+
|
|
84
|
+
setOptions({
|
|
85
|
+
key: KEY,
|
|
86
|
+
v: 'weekly',
|
|
87
|
+
libraries,
|
|
88
|
+
});
|
|
89
|
+
initialised = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// importLibrary('maps') triggers the script injection on first call.
|
|
93
|
+
// We don't need the returned namespace itself — once 'maps' has loaded,
|
|
94
|
+
// every google.maps.* member is on the global `google` object, so we
|
|
95
|
+
// return that for callers to destructure as they wish.
|
|
96
|
+
loaderPromise = importLibrary('maps').then(() => google.maps);
|
|
97
|
+
|
|
98
|
+
return loaderPromise;
|
|
99
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser geolocation consent helper.
|
|
5
|
+
*
|
|
6
|
+
* Returns a hook that requests the user's coordinates on demand. Coords are
|
|
7
|
+
* cached in component state and forwarded as `X-Browser-Latitude` /
|
|
8
|
+
* `X-Browser-Longitude` headers on every Inertia visit. The hook is a soft
|
|
9
|
+
* prompt — login/register still work when the user denies permission, but
|
|
10
|
+
* the form payload omits the coords and the server stores null.
|
|
11
|
+
*
|
|
12
|
+
* attachLocationToInertiaVisits(router) accepts Inertia's router from the
|
|
13
|
+
* consuming app and is a no-op when omitted (Storybook / Next.js).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
17
|
+
|
|
18
|
+
interface LocationRouter {
|
|
19
|
+
on: (event: string, callback: (e: unknown) => void) => () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Coords = {
|
|
23
|
+
latitude: number;
|
|
24
|
+
longitude: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ConsentStatus =
|
|
28
|
+
| 'idle'
|
|
29
|
+
| 'prompting'
|
|
30
|
+
| 'granted'
|
|
31
|
+
| 'denied'
|
|
32
|
+
| 'unavailable';
|
|
33
|
+
|
|
34
|
+
let _cached: Coords | null = null;
|
|
35
|
+
|
|
36
|
+
export function cachedCoords(): Coords | null {
|
|
37
|
+
return _cached;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setCachedCoords(coords: Coords | null): void {
|
|
41
|
+
_cached = coords;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useLocationConsent(): {
|
|
45
|
+
coords: Coords | null;
|
|
46
|
+
status: ConsentStatus;
|
|
47
|
+
request: () => Promise<Coords | null>;
|
|
48
|
+
} {
|
|
49
|
+
const [coords, setCoords] = useState<Coords | null>(_cached);
|
|
50
|
+
const [status, setStatus] = useState<ConsentStatus>(
|
|
51
|
+
_cached ? 'granted' : 'idle',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const request = useCallback((): Promise<Coords | null> => {
|
|
55
|
+
if (typeof window === 'undefined' || !navigator.geolocation) {
|
|
56
|
+
setStatus('unavailable');
|
|
57
|
+
|
|
58
|
+
return Promise.resolve(null);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setStatus('prompting');
|
|
62
|
+
|
|
63
|
+
return new Promise<Coords | null>((resolve) => {
|
|
64
|
+
navigator.geolocation.getCurrentPosition(
|
|
65
|
+
(position) => {
|
|
66
|
+
const next: Coords = {
|
|
67
|
+
latitude: position.coords.latitude,
|
|
68
|
+
longitude: position.coords.longitude,
|
|
69
|
+
};
|
|
70
|
+
_cached = next;
|
|
71
|
+
setCoords(next);
|
|
72
|
+
setStatus('granted');
|
|
73
|
+
resolve(next);
|
|
74
|
+
},
|
|
75
|
+
() => {
|
|
76
|
+
setStatus('denied');
|
|
77
|
+
resolve(null);
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
enableHighAccuracy: false,
|
|
81
|
+
maximumAge: 60_000,
|
|
82
|
+
timeout: 8_000,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
return { coords, status, request };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wire cached coordinates onto every outgoing Inertia visit as request
|
|
93
|
+
* headers. Idempotent — safe to call once at app bootstrap.
|
|
94
|
+
*/
|
|
95
|
+
export function attachLocationToInertiaVisits(router?: LocationRouter): void {
|
|
96
|
+
if (typeof window === 'undefined' || !router) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
router.on('before', (event: unknown) => {
|
|
101
|
+
if (!_cached) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const headers = (event as { detail: { visit?: { headers?: Record<string, string> } } }).detail.visit?.headers ?? {};
|
|
106
|
+
(headers as Record<string, string>)['X-Browser-Latitude'] = String(
|
|
107
|
+
_cached.latitude,
|
|
108
|
+
);
|
|
109
|
+
(headers as Record<string, string>)['X-Browser-Longitude'] = String(
|
|
110
|
+
_cached.longitude,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Convenience: re-request location on mount, swallowing failures. */
|
|
116
|
+
export function useEagerLocationConsent(): ConsentStatus {
|
|
117
|
+
const { status, request } = useLocationConsent();
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (status === 'idle') {
|
|
121
|
+
void request();
|
|
122
|
+
}
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
return status;
|
|
127
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Activity,
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
ArrowDown,
|
|
5
|
+
ArrowLeft,
|
|
6
|
+
ArrowRight,
|
|
7
|
+
ArrowUp,
|
|
8
|
+
Battery,
|
|
9
|
+
Bell,
|
|
10
|
+
BookOpen,
|
|
11
|
+
Building2,
|
|
12
|
+
Calendar,
|
|
13
|
+
Check,
|
|
14
|
+
CheckCircle,
|
|
15
|
+
ChevronDown,
|
|
16
|
+
ChevronRight,
|
|
17
|
+
Cloud,
|
|
18
|
+
Code,
|
|
19
|
+
Cpu,
|
|
20
|
+
Database,
|
|
21
|
+
Download,
|
|
22
|
+
Droplets,
|
|
23
|
+
ExternalLink,
|
|
24
|
+
Factory,
|
|
25
|
+
Globe,
|
|
26
|
+
GraduationCap,
|
|
27
|
+
HardHat,
|
|
28
|
+
Headphones,
|
|
29
|
+
Heart,
|
|
30
|
+
Image as ImageIcon,
|
|
31
|
+
Landmark,
|
|
32
|
+
Leaf,
|
|
33
|
+
Mail,
|
|
34
|
+
MapPin,
|
|
35
|
+
Newspaper,
|
|
36
|
+
Package,
|
|
37
|
+
Phone,
|
|
38
|
+
Plane,
|
|
39
|
+
Play,
|
|
40
|
+
Radio,
|
|
41
|
+
RefreshCw,
|
|
42
|
+
Rocket,
|
|
43
|
+
Search,
|
|
44
|
+
Shield,
|
|
45
|
+
Ship,
|
|
46
|
+
ShoppingBag,
|
|
47
|
+
ShoppingCart,
|
|
48
|
+
Sparkles,
|
|
49
|
+
Star,
|
|
50
|
+
Stethoscope,
|
|
51
|
+
Thermometer,
|
|
52
|
+
Tractor,
|
|
53
|
+
Truck,
|
|
54
|
+
Users,
|
|
55
|
+
Wheat,
|
|
56
|
+
Zap,
|
|
57
|
+
} from 'lucide-react';
|
|
58
|
+
import type { LucideIcon } from 'lucide-react';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* String → LucideIcon lookup for CMS content (icons stored as PascalCase
|
|
62
|
+
* lucide-react export names in section JSON). Keep this list aligned with
|
|
63
|
+
* the icon options in Filament resource Selects (icon_name fields).
|
|
64
|
+
*
|
|
65
|
+
* To add a new icon: import it above, add it to the map below, and add
|
|
66
|
+
* it to the matching Filament Select options array.
|
|
67
|
+
*/
|
|
68
|
+
export const LUCIDE_ICON_MAP: Record<string, LucideIcon> = {
|
|
69
|
+
Activity,
|
|
70
|
+
AlertTriangle,
|
|
71
|
+
ArrowDown,
|
|
72
|
+
ArrowLeft,
|
|
73
|
+
ArrowRight,
|
|
74
|
+
ArrowUp,
|
|
75
|
+
Battery,
|
|
76
|
+
Bell,
|
|
77
|
+
BookOpen,
|
|
78
|
+
Building2,
|
|
79
|
+
Calendar,
|
|
80
|
+
Check,
|
|
81
|
+
CheckCircle,
|
|
82
|
+
ChevronDown,
|
|
83
|
+
ChevronRight,
|
|
84
|
+
Cloud,
|
|
85
|
+
Code,
|
|
86
|
+
Cpu,
|
|
87
|
+
Database,
|
|
88
|
+
Download,
|
|
89
|
+
Droplets,
|
|
90
|
+
ExternalLink,
|
|
91
|
+
Factory,
|
|
92
|
+
Globe,
|
|
93
|
+
GraduationCap,
|
|
94
|
+
HardHat,
|
|
95
|
+
Headphones,
|
|
96
|
+
Heart,
|
|
97
|
+
Image: ImageIcon,
|
|
98
|
+
Landmark,
|
|
99
|
+
Leaf,
|
|
100
|
+
Mail,
|
|
101
|
+
MapPin,
|
|
102
|
+
Newspaper,
|
|
103
|
+
Package,
|
|
104
|
+
Phone,
|
|
105
|
+
Plane,
|
|
106
|
+
Play,
|
|
107
|
+
Radio,
|
|
108
|
+
RefreshCw,
|
|
109
|
+
Rocket,
|
|
110
|
+
Search,
|
|
111
|
+
Shield,
|
|
112
|
+
Ship,
|
|
113
|
+
ShoppingBag,
|
|
114
|
+
ShoppingCart,
|
|
115
|
+
Sparkles,
|
|
116
|
+
Star,
|
|
117
|
+
Stethoscope,
|
|
118
|
+
Thermometer,
|
|
119
|
+
Tractor,
|
|
120
|
+
Truck,
|
|
121
|
+
Users,
|
|
122
|
+
Wheat,
|
|
123
|
+
Zap,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export function lucideIcon(name?: string | null): LucideIcon | null {
|
|
127
|
+
if (!name) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return LUCIDE_ICON_MAP[name] ?? null;
|
|
132
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import arrowBlue from '../assets/map/arrows/map-arrow-blue.png';
|
|
2
|
+
import arrowGreen from '../assets/map/arrows/map-arrow-green.png';
|
|
3
|
+
import arrowPurple from '../assets/map/arrows/map-arrow-purple.png';
|
|
4
|
+
import arrowRed from '../assets/map/arrows/map-arrow-red.png';
|
|
5
|
+
|
|
6
|
+
import flagBlue from '../assets/map/flags/flag-blue.png';
|
|
7
|
+
import flagGreen from '../assets/map/flags/flag-green.png';
|
|
8
|
+
import flagRed from '../assets/map/flags/flag-red.png';
|
|
9
|
+
import flagYellow from '../assets/map/flags/flag-yellow.png';
|
|
10
|
+
|
|
11
|
+
// ─── Network quality ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Four-tier signal classification.
|
|
15
|
+
* Input is a 0–100 percentage (normalise before calling if the device
|
|
16
|
+
* sends a raw dBm or 0–4 bar value).
|
|
17
|
+
*
|
|
18
|
+
* | Tier | Range | Colour |
|
|
19
|
+
* |------------|--------|---------|
|
|
20
|
+
* | none/low | 0–25 | red |
|
|
21
|
+
* | above-low | 26–50 | purple |
|
|
22
|
+
* | average | 51–75 | blue |
|
|
23
|
+
* | good | 76–100 | green |
|
|
24
|
+
*/
|
|
25
|
+
export type NetworkTier = 'none' | 'low' | 'above-low' | 'average' | 'good';
|
|
26
|
+
export type MarkerColor = 'red' | 'purple' | 'blue' | 'green';
|
|
27
|
+
|
|
28
|
+
export function networkTier(signal: number | null | undefined): NetworkTier {
|
|
29
|
+
if (signal == null || signal <= 0) return 'none';
|
|
30
|
+
if (signal <= 25) return 'low';
|
|
31
|
+
if (signal <= 50) return 'above-low';
|
|
32
|
+
if (signal <= 75) return 'average';
|
|
33
|
+
return 'good';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Maps any signal strength (0–100) to its marker colour. */
|
|
37
|
+
export function markerColor(signal: number | null | undefined): MarkerColor {
|
|
38
|
+
const tier = networkTier(signal);
|
|
39
|
+
if (tier === 'none' || tier === 'low') return 'red';
|
|
40
|
+
if (tier === 'above-low') return 'purple';
|
|
41
|
+
if (tier === 'average') return 'blue';
|
|
42
|
+
return 'green';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Arrow markers (device has a known heading) ───────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** Arrow images keyed by colour. All images point north (up = 0°). */
|
|
48
|
+
export const ARROW_URLS: Record<MarkerColor, string> = {
|
|
49
|
+
red: arrowRed,
|
|
50
|
+
purple: arrowPurple,
|
|
51
|
+
blue: arrowBlue,
|
|
52
|
+
green: arrowGreen,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* CSS `transform` string for a north-pointing arrow image.
|
|
57
|
+
* Returns an empty string when heading is unknown (renders the image unrotated,
|
|
58
|
+
* but in practice callers should show a pin instead — see `useArrow`).
|
|
59
|
+
*
|
|
60
|
+
* @param heading - Degrees clockwise from north (0–360).
|
|
61
|
+
*/
|
|
62
|
+
export function arrowRotation(heading: number | null | undefined): string {
|
|
63
|
+
if (heading == null) return '';
|
|
64
|
+
return `rotate(${(heading % 360 + 360) % 360}deg)`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether to show an arrow (has heading) or a static pin (no heading).
|
|
69
|
+
* Call this before deciding which marker type to render.
|
|
70
|
+
*/
|
|
71
|
+
export function useArrow(heading: number | null | undefined): boolean {
|
|
72
|
+
return heading != null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the arrow image URL for a device, given its network signal.
|
|
77
|
+
* Only call this when `useArrow(device.heading)` is true.
|
|
78
|
+
*/
|
|
79
|
+
export function deviceArrowUrl(signal: number | null | undefined): string {
|
|
80
|
+
return ARROW_URLS[markerColor(signal)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Pin colours (device has no heading) ─────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Hex fill colours for the static pin marker (rendered as a DOM element,
|
|
87
|
+
* no separate image needed).
|
|
88
|
+
*/
|
|
89
|
+
export const PIN_COLORS: Record<MarkerColor, string> = {
|
|
90
|
+
red: '#ef4444',
|
|
91
|
+
purple: '#a855f7',
|
|
92
|
+
blue: '#3b82f6',
|
|
93
|
+
green: '#22c55e',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Returns the hex pin colour for a device, given its network signal. */
|
|
97
|
+
export function devicePinColor(signal: number | null | undefined): string {
|
|
98
|
+
return PIN_COLORS[markerColor(signal)];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Incident flags ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Incident flags mark the location where an incident was last opened.
|
|
105
|
+
* Colour reflects the incident's priority/severity.
|
|
106
|
+
*/
|
|
107
|
+
export type IncidentSeverity = 'critical' | 'high' | 'medium' | 'low';
|
|
108
|
+
|
|
109
|
+
export const FLAG_URLS: Record<IncidentSeverity, string> = {
|
|
110
|
+
critical: flagRed,
|
|
111
|
+
high: flagYellow,
|
|
112
|
+
medium: flagBlue,
|
|
113
|
+
low: flagGreen,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Returns the flag image URL for an incident. */
|
|
117
|
+
export function incidentFlagUrl(severity: IncidentSeverity): string {
|
|
118
|
+
return FLAG_URLS[severity];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Named re-exports ─────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export const arrows = { red: arrowRed, purple: arrowPurple, blue: arrowBlue, green: arrowGreen };
|
|
124
|
+
export const flags = { red: flagRed, yellow: flagYellow, blue: flagBlue, green: flagGreen };
|