@trackany-device/components 1.0.0 → 1.2.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/README.md +216 -0
- package/package.json +133 -4
- package/src/assets/index.ts +120 -0
- package/src/assets/media/avatars/300-1.png +0 -0
- package/src/assets/media/avatars/300-10.png +0 -0
- package/src/assets/media/avatars/300-11.png +0 -0
- package/src/assets/media/avatars/300-12.png +0 -0
- package/src/assets/media/avatars/300-13.png +0 -0
- package/src/assets/media/avatars/300-14.png +0 -0
- package/src/assets/media/avatars/300-15.png +0 -0
- package/src/assets/media/avatars/300-16.png +0 -0
- package/src/assets/media/avatars/300-17.png +0 -0
- package/src/assets/media/avatars/300-18.png +0 -0
- package/src/assets/media/avatars/300-19.png +0 -0
- package/src/assets/media/avatars/300-2.png +0 -0
- package/src/assets/media/avatars/300-20.png +0 -0
- package/src/assets/media/avatars/300-21.png +0 -0
- package/src/assets/media/avatars/300-22.png +0 -0
- package/src/assets/media/avatars/300-23.png +0 -0
- package/src/assets/media/avatars/300-24.png +0 -0
- package/src/assets/media/avatars/300-25.png +0 -0
- package/src/assets/media/avatars/300-26.png +0 -0
- package/src/assets/media/avatars/300-27.png +0 -0
- package/src/assets/media/avatars/300-28.png +0 -0
- package/src/assets/media/avatars/300-29.png +0 -0
- package/src/assets/media/avatars/300-3.png +0 -0
- package/src/assets/media/avatars/300-30.png +0 -0
- package/src/assets/media/avatars/300-31.png +0 -0
- package/src/assets/media/avatars/300-32.png +0 -0
- package/src/assets/media/avatars/300-33.png +0 -0
- package/src/assets/media/avatars/300-34.png +0 -0
- package/src/assets/media/avatars/300-4.png +0 -0
- package/src/assets/media/avatars/300-5.png +0 -0
- package/src/assets/media/avatars/300-6.png +0 -0
- package/src/assets/media/avatars/300-7.png +0 -0
- package/src/assets/media/avatars/300-8.png +0 -0
- package/src/assets/media/avatars/300-9.png +0 -0
- package/src/assets/media/avatars/blank.png +0 -0
- package/src/assets/media/avatars/gray/1.png +0 -0
- package/src/assets/media/avatars/gray/2.png +0 -0
- package/src/assets/media/avatars/gray/3.png +0 -0
- package/src/assets/media/avatars/gray/4.png +0 -0
- package/src/assets/media/avatars/gray/5.png +0 -0
- package/src/assets/media/illustrations/1-dark.svg +78 -0
- package/src/assets/media/illustrations/1.svg +78 -0
- package/src/assets/media/illustrations/10-dark.svg +148 -0
- package/src/assets/media/illustrations/10.svg +148 -0
- package/src/assets/media/illustrations/11-dark.svg +234 -0
- package/src/assets/media/illustrations/11.svg +234 -0
- package/src/assets/media/illustrations/12.svg +138 -0
- package/src/assets/media/illustrations/13.svg +205 -0
- package/src/assets/media/illustrations/14.svg +259 -0
- package/src/assets/media/illustrations/15.svg +242 -0
- package/src/assets/media/illustrations/16.svg +128 -0
- package/src/assets/media/illustrations/17.svg +180 -0
- package/src/assets/media/illustrations/18-dark.svg +6 -0
- package/src/assets/media/illustrations/18.svg +6 -0
- package/src/assets/media/illustrations/19-dark.svg +8 -0
- package/src/assets/media/illustrations/19.svg +8 -0
- package/src/assets/media/illustrations/2-dark.svg +78 -0
- package/src/assets/media/illustrations/2.svg +78 -0
- package/src/assets/media/illustrations/20-dark.svg +13 -0
- package/src/assets/media/illustrations/20.svg +13 -0
- package/src/assets/media/illustrations/21-dark.svg +9 -0
- package/src/assets/media/illustrations/21.svg +9 -0
- package/src/assets/media/illustrations/22-dark.svg +17 -0
- package/src/assets/media/illustrations/22.svg +17 -0
- package/src/assets/media/illustrations/23-dark.svg +13 -0
- package/src/assets/media/illustrations/23.svg +13 -0
- package/src/assets/media/illustrations/24.svg +6 -0
- package/src/assets/media/illustrations/25.svg +8 -0
- package/src/assets/media/illustrations/26.svg +8 -0
- package/src/assets/media/illustrations/27.svg +6 -0
- package/src/assets/media/illustrations/28-dark.svg +28 -0
- package/src/assets/media/illustrations/28.svg +14 -0
- package/src/assets/media/illustrations/29-dark.svg +6 -0
- package/src/assets/media/illustrations/29.svg +6 -0
- package/src/assets/media/illustrations/3-dark.svg +70 -0
- package/src/assets/media/illustrations/3.svg +70 -0
- package/src/assets/media/illustrations/30-dark.svg +8 -0
- package/src/assets/media/illustrations/30.svg +8 -0
- package/src/assets/media/illustrations/31-dark.svg +9 -0
- package/src/assets/media/illustrations/31.svg +9 -0
- package/src/assets/media/illustrations/32-dark.svg +10 -0
- package/src/assets/media/illustrations/32.svg +10 -0
- package/src/assets/media/illustrations/33-dark.svg +15 -0
- package/src/assets/media/illustrations/33.svg +15 -0
- package/src/assets/media/illustrations/34-dark.svg +5 -0
- package/src/assets/media/illustrations/34.svg +5 -0
- package/src/assets/media/illustrations/35-dark.svg +11 -0
- package/src/assets/media/illustrations/35.svg +4 -0
- package/src/assets/media/illustrations/4-dark.svg +51 -0
- package/src/assets/media/illustrations/4.svg +51 -0
- package/src/assets/media/illustrations/5-dark.svg +78 -0
- package/src/assets/media/illustrations/5.svg +78 -0
- package/src/assets/media/illustrations/6.svg +58 -0
- package/src/assets/media/illustrations/7.svg +49 -0
- package/src/assets/media/illustrations/8.svg +61 -0
- package/src/assets/media/illustrations/9.svg +57 -0
- package/src/assets/media/misc/placeholder.svg +15 -0
- package/src/components/devices/devices-mini-map.tsx +32 -26
- package/src/components/devices/map-marker.tsx +98 -0
- package/src/components/ui/checklist-item.tsx +55 -0
- package/src/components/ui/plan-card.tsx +68 -0
- package/src/components/ui/settings-row.tsx +32 -0
- package/src/components/ui/settings-section.tsx +22 -0
- package/src/components/ui/usage-meter.tsx +35 -0
- package/src/index.ts +12 -1
- package/src/layouts/LayoutSwitcher.tsx +220 -0
- package/src/layouts/app/MegaMenuLayout.tsx +69 -34
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +73 -37
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +53 -4
- package/src/layouts/app/NavbarSidebarLayout.tsx +74 -29
- package/src/layouts/app/SidebarDualMenuLayout.tsx +48 -5
- package/src/layouts/app/SidebarFixedLayout.tsx +15 -10
- package/src/layouts/app/SidebarMinimalLayout.tsx +51 -3
- package/src/layouts/app/SidebarTabsLayout.tsx +48 -2
- package/src/layouts/app/SplitSidebarLayout.tsx +91 -43
- package/src/layouts/app/TopNavLayout.tsx +7 -12
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +103 -46
- package/src/layouts/app/partials/Navbar.tsx +61 -10
- package/src/layouts/app/partials/Toolbar.tsx +1 -1
- package/src/layouts/auth/AuthCenteredLayout.tsx +10 -4
- package/src/lib/map-markers.ts +21 -3
- package/src/pages/login/ConfirmPasswordPage.tsx +35 -0
- package/src/pages/login/ForgotPasswordPage.tsx +41 -0
- package/src/pages/login/LoginPage.tsx +50 -0
- package/src/pages/login/RegisterPage.tsx +41 -0
- package/src/pages/login/ResetPasswordPage.tsx +35 -0
- package/src/pages/login/TwoFactorChallengePage.tsx +41 -0
- package/src/pages/login/VerifyEmailPage.tsx +31 -0
- package/src/pages/my/ActivityPage.tsx +160 -0
- package/src/pages/my/GetStartedPage.tsx +221 -0
- package/src/pages/my/NotificationsPage.tsx +133 -0
- package/src/pages/my/ProfilePage.tsx +650 -0
- package/src/pages/my/TenantsPage.tsx +37 -0
- package/src/pages/tenant/AssigneesPage.tsx +155 -0
- package/src/pages/tenant/BeatsPage.tsx +403 -0
- package/src/pages/tenant/DashboardPage.tsx +195 -0
- package/src/pages/tenant/GeofencePage.tsx +422 -0
- package/src/pages/tenant/IncidentsPage.tsx +214 -0
- package/src/pages/tenant/IntegrationsPage.tsx +352 -0
- package/src/pages/tenant/InvitePage.tsx +153 -0
- package/src/pages/tenant/LiveStreamPage.tsx +141 -0
- package/src/pages/tenant/MembersPage.tsx +414 -0
- package/src/pages/tenant/TenantProfilePage.tsx +701 -0
- package/src/platform/adapters/default.tsx +1 -1
- package/src/platform/types.ts +2 -0
- package/src/styles/components/apexcharts.css +101 -0
- package/src/styles/components/image-input.css +51 -0
- package/src/styles/components/leaflet.css +25 -0
- package/src/styles/components/rating.css +89 -0
- package/src/styles/components/scrollable.css +119 -0
- package/src/styles/layout.css +24 -0
- package/src/styles/layouts/sidebar-fixed.css +93 -138
- package/src/styles/themes.css +5 -5
- package/src/vite-env.d.ts +21 -0
- package/src/layouts/SettingsLayout.tsx +0 -21
- package/src/layouts/app-layout.tsx +0 -29
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FormEvent } from 'react';
|
|
2
|
+
import type { AuthLayoutVariant } from '../../index';
|
|
3
|
+
import type { OtpFormErrors } from '../../elements/OtpForm';
|
|
4
|
+
import { OtpForm } from '../../elements/OtpForm';
|
|
5
|
+
import { AuthLayoutResolved } from '../../layouts/LayoutSwitcher';
|
|
6
|
+
|
|
7
|
+
export default function TwoFactorChallengePage({
|
|
8
|
+
authLayout,
|
|
9
|
+
code,
|
|
10
|
+
setCode,
|
|
11
|
+
recovery = false,
|
|
12
|
+
setRecovery,
|
|
13
|
+
errors = {},
|
|
14
|
+
processing = false,
|
|
15
|
+
title = 'Two-factor authentication',
|
|
16
|
+
description = 'Verify your identity to continue',
|
|
17
|
+
}: {
|
|
18
|
+
authLayout: AuthLayoutVariant;
|
|
19
|
+
code: string;
|
|
20
|
+
setCode: (v: string) => void;
|
|
21
|
+
recovery?: boolean;
|
|
22
|
+
setRecovery: (v: boolean) => void;
|
|
23
|
+
errors?: OtpFormErrors;
|
|
24
|
+
processing?: boolean;
|
|
25
|
+
title?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<AuthLayoutResolved variant={authLayout} title={title} description={description}>
|
|
30
|
+
<OtpForm
|
|
31
|
+
code={code}
|
|
32
|
+
errors={errors}
|
|
33
|
+
processing={processing}
|
|
34
|
+
recovery={recovery}
|
|
35
|
+
onCodeChange={setCode}
|
|
36
|
+
onSubmit={(e: FormEvent) => e.preventDefault()}
|
|
37
|
+
onToggleRecovery={() => setRecovery(!recovery)}
|
|
38
|
+
/>
|
|
39
|
+
</AuthLayoutResolved>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FormEvent } from 'react';
|
|
2
|
+
import type { AuthLayoutVariant } from '../../index';
|
|
3
|
+
import { VerifyEmailForm } from '../../elements/VerifyEmailForm';
|
|
4
|
+
import { AuthLayoutResolved } from '../../layouts/LayoutSwitcher';
|
|
5
|
+
|
|
6
|
+
export default function VerifyEmailPage({
|
|
7
|
+
authLayout,
|
|
8
|
+
processing = false,
|
|
9
|
+
status,
|
|
10
|
+
logoutUrl = '/logout',
|
|
11
|
+
title = 'Verify your email',
|
|
12
|
+
description = 'Check your inbox for a verification link',
|
|
13
|
+
}: {
|
|
14
|
+
authLayout: AuthLayoutVariant;
|
|
15
|
+
processing?: boolean;
|
|
16
|
+
status?: string;
|
|
17
|
+
logoutUrl?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<AuthLayoutResolved variant={authLayout} title={title} description={description}>
|
|
23
|
+
<VerifyEmailForm
|
|
24
|
+
processing={processing}
|
|
25
|
+
status={status}
|
|
26
|
+
logoutUrl={logoutUrl}
|
|
27
|
+
onSubmit={(e: FormEvent) => e.preventDefault()}
|
|
28
|
+
/>
|
|
29
|
+
</AuthLayoutResolved>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Avatar, AvatarFallback,
|
|
3
|
+
Badge, Button,
|
|
4
|
+
Card, CardContent, CardHeader, CardTitle,
|
|
5
|
+
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
6
|
+
Timeline, TimelineItem,
|
|
7
|
+
} from '@trackany-device/components';
|
|
8
|
+
import { LayoutResolved } from '../../layouts/LayoutSwitcher';
|
|
9
|
+
import type { LayoutName } from '../../layouts/LayoutSwitcher';
|
|
10
|
+
import { Download, Filter, Search, type LucideIcon } from 'lucide-react';
|
|
11
|
+
import type { ElementType } from 'react';
|
|
12
|
+
|
|
13
|
+
export type ActivityVariant = 'default' | 'danger' | 'warning' | 'success' | 'info';
|
|
14
|
+
|
|
15
|
+
export interface ActivityItem {
|
|
16
|
+
icon: LucideIcon;
|
|
17
|
+
title: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
datetime: string;
|
|
20
|
+
variant: ActivityVariant;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LoginEntry {
|
|
24
|
+
device: string;
|
|
25
|
+
location: string;
|
|
26
|
+
ip: string;
|
|
27
|
+
date: string;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FeedItem {
|
|
32
|
+
initials: string;
|
|
33
|
+
name: string;
|
|
34
|
+
action: string;
|
|
35
|
+
time: string;
|
|
36
|
+
badge: { label: string; cls: string };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ActivityTimelinePage({ layout, items }: { layout: LayoutName; items: ActivityItem[] }) {
|
|
40
|
+
return (
|
|
41
|
+
<LayoutResolved layout={layout} title="Activity" currentUrl="/activity">
|
|
42
|
+
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
|
43
|
+
<div className="flex items-center justify-between">
|
|
44
|
+
<h1 className="text-xl font-semibold">Activity</h1>
|
|
45
|
+
<div className="flex gap-2">
|
|
46
|
+
<Button variant="outline" size="sm"><Filter className="h-3.5 w-3.5 mr-1.5" />Filter</Button>
|
|
47
|
+
<Button variant="outline" size="sm"><Download className="h-3.5 w-3.5 mr-1.5" />Export</Button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader><CardTitle>Recent activity</CardTitle></CardHeader>
|
|
52
|
+
<CardContent>
|
|
53
|
+
<Timeline>
|
|
54
|
+
{items.map((item, i) => (
|
|
55
|
+
<TimelineItem key={i} icon={item.icon} title={item.title} datetime={item.datetime} variant={item.variant}>
|
|
56
|
+
{item.detail}
|
|
57
|
+
</TimelineItem>
|
|
58
|
+
))}
|
|
59
|
+
</Timeline>
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
</div>
|
|
63
|
+
</LayoutResolved>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function LoginHistoryPage({ layout, logins }: { layout: LayoutName; logins: LoginEntry[] }) {
|
|
68
|
+
return (
|
|
69
|
+
<LayoutResolved layout={layout} title="Login History" currentUrl="/activity/login">
|
|
70
|
+
<div className="p-6 max-w-3xl mx-auto space-y-6">
|
|
71
|
+
<div className="flex items-center justify-between">
|
|
72
|
+
<h1 className="text-xl font-semibold">Login history</h1>
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<div className="relative">
|
|
75
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
76
|
+
<input placeholder="Search…" className="pl-8 pr-3 py-1.5 text-sm rounded-lg border border-border bg-background" />
|
|
77
|
+
</div>
|
|
78
|
+
<Button variant="outline" size="sm"><Download className="h-3.5 w-3.5 mr-1.5" />Export</Button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<Card>
|
|
83
|
+
<CardContent className="p-0">
|
|
84
|
+
<Table>
|
|
85
|
+
<TableHeader>
|
|
86
|
+
<TableRow>
|
|
87
|
+
<TableHead>Device</TableHead>
|
|
88
|
+
<TableHead>Location</TableHead>
|
|
89
|
+
<TableHead>IP address</TableHead>
|
|
90
|
+
<TableHead>Date & time</TableHead>
|
|
91
|
+
<TableHead>Status</TableHead>
|
|
92
|
+
</TableRow>
|
|
93
|
+
</TableHeader>
|
|
94
|
+
<TableBody>
|
|
95
|
+
{logins.map((row, i) => (
|
|
96
|
+
<TableRow key={i}>
|
|
97
|
+
<TableCell className="text-sm font-medium">{row.device}</TableCell>
|
|
98
|
+
<TableCell className="text-sm text-muted-foreground">{row.location}</TableCell>
|
|
99
|
+
<TableCell className="text-xs font-mono text-muted-foreground">{row.ip}</TableCell>
|
|
100
|
+
<TableCell className="text-sm text-muted-foreground">{row.date}</TableCell>
|
|
101
|
+
<TableCell>
|
|
102
|
+
<Badge variant="outline" className={`text-xs ${row.ok ? 'text-green-600 border-green-200 bg-green-50' : 'text-red-600 border-red-200 bg-red-50'}`}>
|
|
103
|
+
{row.ok ? 'Success' : 'Failed'}
|
|
104
|
+
</Badge>
|
|
105
|
+
</TableCell>
|
|
106
|
+
</TableRow>
|
|
107
|
+
))}
|
|
108
|
+
</TableBody>
|
|
109
|
+
</Table>
|
|
110
|
+
</CardContent>
|
|
111
|
+
</Card>
|
|
112
|
+
|
|
113
|
+
<Card>
|
|
114
|
+
<CardHeader><CardTitle>Login statistics</CardTitle></CardHeader>
|
|
115
|
+
<CardContent>
|
|
116
|
+
<div className="grid grid-cols-3 gap-4 text-center">
|
|
117
|
+
{[
|
|
118
|
+
{ label: 'Total logins', value: String(logins.length * 30), sub: 'Last 90 days' },
|
|
119
|
+
{ label: 'Failed attempts', value: String(logins.filter((l) => !l.ok).length), sub: 'Last 90 days', warn: true },
|
|
120
|
+
{ label: 'Unique devices', value: String(new Set(logins.map((l) => l.device)).size), sub: 'Across sessions' },
|
|
121
|
+
].map(({ label, value, sub, warn }) => (
|
|
122
|
+
<div key={label} className="rounded-lg border border-border p-4">
|
|
123
|
+
<p className={`text-2xl font-bold ${warn ? 'text-destructive' : ''}`}>{value}</p>
|
|
124
|
+
<p className="text-sm font-medium mt-0.5">{label}</p>
|
|
125
|
+
<p className="text-xs text-muted-foreground mt-0.5">{sub}</p>
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
</div>
|
|
132
|
+
</LayoutResolved>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function AccountActivityFeedPage({ layout, items }: { layout: LayoutName; items: FeedItem[] }) {
|
|
137
|
+
return (
|
|
138
|
+
<LayoutResolved layout={layout} title="Activity Feed" currentUrl="/activity/feed">
|
|
139
|
+
<div className="p-6 max-w-xl mx-auto space-y-6">
|
|
140
|
+
<h1 className="text-xl font-semibold">Activity feed</h1>
|
|
141
|
+
<Card>
|
|
142
|
+
<CardContent className="p-0">
|
|
143
|
+
{items.map((item, i) => (
|
|
144
|
+
<div key={i} className="flex items-start gap-3 px-5 py-4 border-b border-border last:border-0">
|
|
145
|
+
<Avatar className="h-8 w-8 mt-0.5 shrink-0">
|
|
146
|
+
<AvatarFallback className="text-xs">{item.initials}</AvatarFallback>
|
|
147
|
+
</Avatar>
|
|
148
|
+
<div className="flex-1 min-w-0">
|
|
149
|
+
<p className="text-sm"><span className="font-medium">{item.name}</span> {item.action}</p>
|
|
150
|
+
<p className="text-xs text-muted-foreground mt-0.5">{item.time}</p>
|
|
151
|
+
</div>
|
|
152
|
+
<Badge variant="outline" className={`text-xs shrink-0 ${item.badge.cls}`}>{item.badge.label}</Badge>
|
|
153
|
+
</div>
|
|
154
|
+
))}
|
|
155
|
+
</CardContent>
|
|
156
|
+
</Card>
|
|
157
|
+
</div>
|
|
158
|
+
</LayoutResolved>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge, Button,
|
|
3
|
+
Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
|
|
4
|
+
ChecklistItem,
|
|
5
|
+
Progress,
|
|
6
|
+
} from '@trackany-device/components';
|
|
7
|
+
import { LayoutResolved } from '../../layouts/LayoutSwitcher';
|
|
8
|
+
import type { LayoutName } from '../../layouts/LayoutSwitcher';
|
|
9
|
+
import {
|
|
10
|
+
Bell, CheckCircle2, Download,
|
|
11
|
+
Globe, MapPin, Shield, Smartphone,
|
|
12
|
+
Users, Zap,
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import type { LucideIcon } from 'lucide-react';
|
|
16
|
+
|
|
17
|
+
export interface OnboardingStep {
|
|
18
|
+
icon: LucideIcon;
|
|
19
|
+
title: string;
|
|
20
|
+
desc: string;
|
|
21
|
+
action: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function OnboardingChecklistPage({
|
|
25
|
+
layout,
|
|
26
|
+
steps,
|
|
27
|
+
initialCompleted,
|
|
28
|
+
}: {
|
|
29
|
+
layout: LayoutName;
|
|
30
|
+
steps: OnboardingStep[];
|
|
31
|
+
initialCompleted?: number[];
|
|
32
|
+
}) {
|
|
33
|
+
const [completed, setCompleted] = useState(new Set(initialCompleted ?? []));
|
|
34
|
+
const doneCount = completed.size;
|
|
35
|
+
const total = steps.length;
|
|
36
|
+
const percent = Math.round((doneCount / total) * 100);
|
|
37
|
+
const activeIndex = steps.findIndex((_, i) => !completed.has(i));
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<LayoutResolved layout={layout} title="Get Started" currentUrl="/get-started">
|
|
41
|
+
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
|
42
|
+
<div>
|
|
43
|
+
<h1 className="text-xl font-semibold">Get started</h1>
|
|
44
|
+
<p className="text-sm text-muted-foreground mt-0.5">Complete these steps to get the most out of TAD.</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<Card>
|
|
48
|
+
<CardContent className="p-5">
|
|
49
|
+
<div className="flex items-center justify-between mb-2">
|
|
50
|
+
<p className="text-sm font-medium">{doneCount} of {total} steps completed</p>
|
|
51
|
+
<p className="text-sm font-semibold text-primary">{percent}%</p>
|
|
52
|
+
</div>
|
|
53
|
+
<Progress value={percent} className="h-2" />
|
|
54
|
+
</CardContent>
|
|
55
|
+
</Card>
|
|
56
|
+
|
|
57
|
+
<div className="space-y-3">
|
|
58
|
+
{steps.map((step, i) => (
|
|
59
|
+
<ChecklistItem
|
|
60
|
+
key={i}
|
|
61
|
+
icon={step.icon}
|
|
62
|
+
title={step.title}
|
|
63
|
+
description={step.desc}
|
|
64
|
+
done={completed.has(i)}
|
|
65
|
+
active={i === activeIndex}
|
|
66
|
+
action={step.action ?? undefined}
|
|
67
|
+
onAction={() => {
|
|
68
|
+
const next = new Set(completed);
|
|
69
|
+
if (next.has(i)) next.delete(i); else next.add(i);
|
|
70
|
+
setCompleted(next);
|
|
71
|
+
}}
|
|
72
|
+
className="cursor-pointer"
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</LayoutResolved>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function WelcomeScreenPage({ layout }: { layout: LayoutName }) {
|
|
82
|
+
return (
|
|
83
|
+
<LayoutResolved layout={layout} title="Welcome" currentUrl="/get-started">
|
|
84
|
+
<div className="p-6 max-w-3xl mx-auto space-y-8">
|
|
85
|
+
<div className="text-center space-y-2 pt-4">
|
|
86
|
+
<div className="inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10 mb-2">
|
|
87
|
+
<Zap className="h-8 w-8 text-primary" />
|
|
88
|
+
</div>
|
|
89
|
+
<h1 className="text-2xl font-bold">Welcome to TAD</h1>
|
|
90
|
+
<p className="text-muted-foreground max-w-md mx-auto">
|
|
91
|
+
Track Any Device is your all-in-one fleet intelligence platform. Let's get you set up in a few quick steps.
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="grid grid-cols-3 gap-4">
|
|
96
|
+
{[
|
|
97
|
+
{ icon: Smartphone, title: 'Add a device', desc: 'Pair GPS trackers or phones to your fleet.', cta: 'Add device', primary: true },
|
|
98
|
+
{ icon: Users, title: 'Invite your team', desc: 'Bring operators and supervisors on board.', cta: 'Invite', primary: false },
|
|
99
|
+
{ icon: Globe, title: 'Watch live', desc: 'See all your devices on the real-time map.', cta: 'Open map', primary: false },
|
|
100
|
+
].map(({ icon: Icon, title, desc, cta, primary }) => (
|
|
101
|
+
<Card key={title} className={primary ? 'border-primary/40 bg-primary/5' : ''}>
|
|
102
|
+
<CardContent className="p-5 text-center space-y-3">
|
|
103
|
+
<div className={`inline-flex h-10 w-10 items-center justify-center rounded-xl ${primary ? 'bg-primary/10' : 'bg-muted'}`}>
|
|
104
|
+
<Icon className={`h-5 w-5 ${primary ? 'text-primary' : 'text-muted-foreground'}`} />
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<p className="text-sm font-semibold">{title}</p>
|
|
108
|
+
<p className="text-xs text-muted-foreground mt-0.5">{desc}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<Button size="sm" variant={primary ? 'default' : 'outline'} className="w-full">{cta}</Button>
|
|
111
|
+
</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<Card>
|
|
117
|
+
<CardHeader>
|
|
118
|
+
<CardTitle>Quick tips</CardTitle>
|
|
119
|
+
<CardDescription>Make the most of your TAD subscription.</CardDescription>
|
|
120
|
+
</CardHeader>
|
|
121
|
+
<CardContent className="divide-y divide-border">
|
|
122
|
+
{[
|
|
123
|
+
{ icon: MapPin, tip: 'Set geofences on your most-visited locations to get automatic entry/exit alerts.' },
|
|
124
|
+
{ icon: Bell, tip: 'Enable SOS alerts and make sure your operators know how to trigger them.' },
|
|
125
|
+
{ icon: Shield, tip: 'Turn on 2FA and enforce it for all team members in Security settings.' },
|
|
126
|
+
{ icon: Download, tip: 'Download the TAD mobile app to manage your fleet from anywhere.' },
|
|
127
|
+
].map(({ icon: Icon, tip }, i) => (
|
|
128
|
+
<div key={i} className="flex items-start gap-3 py-3.5">
|
|
129
|
+
<div className="rounded-lg bg-muted p-1.5 shrink-0 mt-0.5">
|
|
130
|
+
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
131
|
+
</div>
|
|
132
|
+
<p className="text-sm text-muted-foreground">{tip}</p>
|
|
133
|
+
</div>
|
|
134
|
+
))}
|
|
135
|
+
</CardContent>
|
|
136
|
+
<CardFooter>
|
|
137
|
+
<Button variant="outline" size="sm" className="w-full">View all documentation</Button>
|
|
138
|
+
</CardFooter>
|
|
139
|
+
</Card>
|
|
140
|
+
|
|
141
|
+
<div className="text-center">
|
|
142
|
+
<Button variant="ghost" size="sm" className="text-muted-foreground">Skip onboarding</Button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</LayoutResolved>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function PlanUpgradePage({ layout }: { layout: LayoutName }) {
|
|
150
|
+
return (
|
|
151
|
+
<LayoutResolved layout={layout} title="Upgrade Plan" currentUrl="/get-started/upgrade">
|
|
152
|
+
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
|
153
|
+
<h1 className="text-xl font-semibold">Get more from TAD</h1>
|
|
154
|
+
|
|
155
|
+
<Card className="border-primary/30 bg-gradient-to-br from-primary/5 to-transparent">
|
|
156
|
+
<CardContent className="p-6">
|
|
157
|
+
<div className="flex items-start gap-4">
|
|
158
|
+
<div className="rounded-xl bg-primary/10 p-3 shrink-0">
|
|
159
|
+
<Zap className="h-6 w-6 text-primary" />
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex-1">
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
<p className="font-semibold">You're on the Starter plan</p>
|
|
164
|
+
<Badge variant="outline" className="text-xs">Starter</Badge>
|
|
165
|
+
</div>
|
|
166
|
+
<p className="text-sm text-muted-foreground mt-1">Upgrade to Pro to unlock unlimited devices, advanced analytics, and priority support.</p>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</CardContent>
|
|
170
|
+
</Card>
|
|
171
|
+
|
|
172
|
+
<div className="grid grid-cols-2 gap-4">
|
|
173
|
+
{[
|
|
174
|
+
{
|
|
175
|
+
plan: 'Pro',
|
|
176
|
+
price: '$49',
|
|
177
|
+
period: '/month',
|
|
178
|
+
features: ['Up to 50 devices', 'Advanced analytics', 'Geofence alerts', 'API access', 'Priority support'],
|
|
179
|
+
highlight: true,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
plan: 'Enterprise',
|
|
183
|
+
price: 'Custom',
|
|
184
|
+
period: '',
|
|
185
|
+
features: ['Unlimited devices', 'White-label option', 'Dedicated CSM', 'SLA guarantees', 'SSO & SCIM'],
|
|
186
|
+
highlight: false,
|
|
187
|
+
},
|
|
188
|
+
].map(({ plan, price, period, features, highlight }) => (
|
|
189
|
+
<Card key={plan} className={highlight ? 'border-primary/40' : ''}>
|
|
190
|
+
<CardHeader>
|
|
191
|
+
<div className="flex items-center justify-between">
|
|
192
|
+
<CardTitle className="text-base">{plan}</CardTitle>
|
|
193
|
+
{highlight && <Badge className="text-xs">Popular</Badge>}
|
|
194
|
+
</div>
|
|
195
|
+
<p className="text-2xl font-bold">{price}<span className="text-sm font-normal text-muted-foreground">{period}</span></p>
|
|
196
|
+
</CardHeader>
|
|
197
|
+
<CardContent>
|
|
198
|
+
<ul className="space-y-2">
|
|
199
|
+
{features.map((f) => (
|
|
200
|
+
<li key={f} className="flex items-center gap-2 text-sm">
|
|
201
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-green-600 shrink-0" />{f}
|
|
202
|
+
</li>
|
|
203
|
+
))}
|
|
204
|
+
</ul>
|
|
205
|
+
</CardContent>
|
|
206
|
+
<CardFooter>
|
|
207
|
+
<Button size="sm" className="w-full" variant={highlight ? 'default' : 'outline'}>
|
|
208
|
+
{plan === 'Enterprise' ? 'Contact sales' : 'Upgrade to Pro'}
|
|
209
|
+
</Button>
|
|
210
|
+
</CardFooter>
|
|
211
|
+
</Card>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div className="text-center text-xs text-muted-foreground">
|
|
216
|
+
Questions? <a href="#" className="text-primary underline">Chat with our team</a> or <a href="#" className="text-primary underline">compare all plans</a>.
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</LayoutResolved>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button, Card, CardContent, CardHeader, CardTitle,
|
|
3
|
+
Label, SettingsRow, Switch, Separator,
|
|
4
|
+
} from '@trackany-device/components';
|
|
5
|
+
import { LayoutResolved } from '../../layouts/LayoutSwitcher';
|
|
6
|
+
import type { LayoutName } from '../../layouts/LayoutSwitcher';
|
|
7
|
+
import {
|
|
8
|
+
Bell, BellOff, CalendarClock, ClipboardCheck, DollarSign,
|
|
9
|
+
FileText, Mail, MessageCircle, Monitor, Phone, Tablet, Users,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
export interface UserContact {
|
|
13
|
+
email: string;
|
|
14
|
+
phone: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DigestItem {
|
|
18
|
+
label: string;
|
|
19
|
+
desc: string;
|
|
20
|
+
on: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function NotificationChannelsPage({ layout, user }: { layout: LayoutName; user: UserContact }) {
|
|
24
|
+
return (
|
|
25
|
+
<LayoutResolved layout={layout} title="Notifications" currentUrl="/notifications">
|
|
26
|
+
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
|
27
|
+
<div className="flex items-center justify-between">
|
|
28
|
+
<h1 className="text-xl font-semibold">Notifications</h1>
|
|
29
|
+
<div className="flex items-center gap-2"><Label className="text-sm">Team-wide alerts</Label><Switch /></div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<Card>
|
|
33
|
+
<CardHeader className="pb-0">
|
|
34
|
+
<CardTitle>Notification channels</CardTitle>
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<div>
|
|
37
|
+
<SettingsRow icon={Mail} title="Email" description={user.email} action={<Switch defaultChecked />} />
|
|
38
|
+
<SettingsRow icon={Phone} title="Mobile" description={user.phone} action={<Switch />} />
|
|
39
|
+
<SettingsRow icon={Monitor} title="Desktop" description="Enable real-time desktop browser alerts." action={<Switch defaultChecked />} />
|
|
40
|
+
<SettingsRow
|
|
41
|
+
icon={Bell}
|
|
42
|
+
title="Slack"
|
|
43
|
+
description="Receive instant alerts for messages and updates directly in Slack."
|
|
44
|
+
action={<Button variant="outline" size="sm">Connect Slack</Button>}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</Card>
|
|
48
|
+
|
|
49
|
+
<Card>
|
|
50
|
+
<CardHeader className="pb-0">
|
|
51
|
+
<CardTitle>Fleet alerts</CardTitle>
|
|
52
|
+
</CardHeader>
|
|
53
|
+
<div>
|
|
54
|
+
<SettingsRow icon={Bell} title="Critical incidents" description="SOS pressed, speeding, and high-priority rule violations." action={<Switch defaultChecked />} />
|
|
55
|
+
<SettingsRow icon={Monitor} title="Device offline" description="When a tracked device stops reporting." action={<Switch defaultChecked />} />
|
|
56
|
+
<SettingsRow icon={ClipboardCheck} title="Geofence violations" description="Device enters or exits a restricted zone." action={<Switch defaultChecked />} />
|
|
57
|
+
<SettingsRow icon={Tablet} title="Low battery" description="Battery below 20% on any device." action={<Switch />} />
|
|
58
|
+
<SettingsRow icon={CalendarClock} title="Trip completed" description="Notification when an assignee finishes a trip." action={<Switch />} />
|
|
59
|
+
</div>
|
|
60
|
+
</Card>
|
|
61
|
+
|
|
62
|
+
<Card>
|
|
63
|
+
<CardHeader className="pb-0">
|
|
64
|
+
<CardTitle>Other notifications</CardTitle>
|
|
65
|
+
</CardHeader>
|
|
66
|
+
<div>
|
|
67
|
+
<SettingsRow icon={DollarSign} title="Budget warning" description="Get notified if nearing your device quota limit." action={<Switch defaultChecked />} />
|
|
68
|
+
<SettingsRow icon={FileText} title="Invoice alert" description="Alert for new and unpaid invoices." action={<Button variant="outline" size="sm">View invoices</Button>} />
|
|
69
|
+
<SettingsRow icon={MessageCircle} title="Feedback alert" description="When a team member submits new feedback." action={<Switch defaultChecked />} />
|
|
70
|
+
<SettingsRow icon={Users} title="Collaboration invite" description="Invite to collaborate on a new report or document." action={<Switch defaultChecked />} />
|
|
71
|
+
<SettingsRow icon={ClipboardCheck}title="Status change" description="Notifies changes in incident or task status." action={<Switch defaultChecked />} />
|
|
72
|
+
</div>
|
|
73
|
+
</Card>
|
|
74
|
+
</div>
|
|
75
|
+
</LayoutResolved>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function DoNotDisturbPage({ layout, digestItems }: { layout: LayoutName; digestItems: DigestItem[] }) {
|
|
80
|
+
return (
|
|
81
|
+
<LayoutResolved layout={layout} title="Notifications" currentUrl="/notifications">
|
|
82
|
+
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
|
83
|
+
<h1 className="text-xl font-semibold">Notifications</h1>
|
|
84
|
+
|
|
85
|
+
<Card>
|
|
86
|
+
<CardHeader><CardTitle>Do Not Disturb</CardTitle></CardHeader>
|
|
87
|
+
<CardContent className="space-y-4">
|
|
88
|
+
<p className="text-sm text-muted-foreground">Activate Do Not Disturb to silence all notifications and focus without interruptions during specified hours or tasks.</p>
|
|
89
|
+
<div className="flex items-center justify-between rounded-lg border border-border p-4">
|
|
90
|
+
<div className="flex items-center gap-3">
|
|
91
|
+
<BellOff className="h-5 w-5 text-muted-foreground" />
|
|
92
|
+
<div>
|
|
93
|
+
<p className="text-sm font-medium">Do Not Disturb</p>
|
|
94
|
+
<p className="text-xs text-muted-foreground mt-0.5">All notifications are paused</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<Switch />
|
|
98
|
+
</div>
|
|
99
|
+
<Separator />
|
|
100
|
+
<p className="text-sm font-medium">Scheduled quiet hours</p>
|
|
101
|
+
<div className="grid grid-cols-2 gap-3">
|
|
102
|
+
<div className="space-y-1">
|
|
103
|
+
<Label className="text-xs text-muted-foreground">Start time</Label>
|
|
104
|
+
<input type="time" defaultValue="22:00" className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm" />
|
|
105
|
+
</div>
|
|
106
|
+
<div className="space-y-1">
|
|
107
|
+
<Label className="text-xs text-muted-foreground">End time</Label>
|
|
108
|
+
<input type="time" defaultValue="07:00" className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm" />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex items-center justify-between">
|
|
112
|
+
<div><p className="text-sm font-medium">Allow critical incident alerts</p><p className="text-xs text-muted-foreground">SOS and high-priority violations bypass DND</p></div>
|
|
113
|
+
<Switch defaultChecked />
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex justify-end"><Button size="sm">Save schedule</Button></div>
|
|
116
|
+
</CardContent>
|
|
117
|
+
</Card>
|
|
118
|
+
|
|
119
|
+
<Card>
|
|
120
|
+
<CardHeader><CardTitle>Email digest</CardTitle></CardHeader>
|
|
121
|
+
<CardContent className="space-y-3">
|
|
122
|
+
{digestItems.map(({ label, desc, on }) => (
|
|
123
|
+
<div key={label} className="flex items-center justify-between py-2.5 border-b border-border last:border-0">
|
|
124
|
+
<div><p className="text-sm font-medium">{label}</p><p className="text-xs text-muted-foreground mt-0.5">{desc}</p></div>
|
|
125
|
+
<Switch defaultChecked={on} />
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
</div>
|
|
131
|
+
</LayoutResolved>
|
|
132
|
+
);
|
|
133
|
+
}
|