@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.
Files changed (289) hide show
  1. package/package.json +185 -0
  2. package/src/assets/logo.png +0 -0
  3. package/src/assets/map/arrows/map-arrow-blue.png +0 -0
  4. package/src/assets/map/arrows/map-arrow-green.png +0 -0
  5. package/src/assets/map/arrows/map-arrow-purple.png +0 -0
  6. package/src/assets/map/arrows/map-arrow-red.png +0 -0
  7. package/src/assets/map/flags/flag-blue.png +0 -0
  8. package/src/assets/map/flags/flag-green.png +0 -0
  9. package/src/assets/map/flags/flag-red.png +0 -0
  10. package/src/assets/map/flags/flag-yellow.png +0 -0
  11. package/src/assets/map/pins/map-pin-blue.png +0 -0
  12. package/src/assets/map/pins/map-pin-green.png +0 -0
  13. package/src/assets/map/pins/map-pin-purple.png +0 -0
  14. package/src/assets/map/pins/map-pin-red.png +0 -0
  15. package/src/components/Card.tsx +9 -0
  16. package/src/components/alert-error.tsx +24 -0
  17. package/src/components/app-content.tsx +22 -0
  18. package/src/components/app-header.tsx +153 -0
  19. package/src/components/app-logo-icon.tsx +13 -0
  20. package/src/components/app-logo.tsx +21 -0
  21. package/src/components/app-shell.tsx +19 -0
  22. package/src/components/app-sidebar-header.tsx +68 -0
  23. package/src/components/app-sidebar.tsx +106 -0
  24. package/src/components/appearance-tabs.tsx +46 -0
  25. package/src/components/breadcrumbs.tsx +50 -0
  26. package/src/components/cms/blurred-image.tsx +111 -0
  27. package/src/components/cms/section-bg.tsx +473 -0
  28. package/src/components/cms/section-button.tsx +127 -0
  29. package/src/components/cms/sections/banner-5050-section.tsx +135 -0
  30. package/src/components/cms/sections/blogs-listing-section.tsx +270 -0
  31. package/src/components/cms/sections/cards-grid-section.tsx +185 -0
  32. package/src/components/cms/sections/contact-form-section.tsx +157 -0
  33. package/src/components/cms/sections/cta-section.tsx +101 -0
  34. package/src/components/cms/sections/featured-blog-slider-section.tsx +256 -0
  35. package/src/components/cms/sections/featured-products-grid-section.tsx +173 -0
  36. package/src/components/cms/sections/featured-solutions-grid-section.tsx +183 -0
  37. package/src/components/cms/sections/hero-section.tsx +180 -0
  38. package/src/components/cms/sections/solutions-with-filter-section.tsx +234 -0
  39. package/src/components/cms/sections/text-section.tsx +77 -0
  40. package/src/components/cutout-image.tsx +228 -0
  41. package/src/components/devices/devices-mini-map.tsx +275 -0
  42. package/src/components/docs/docs-shell.tsx +280 -0
  43. package/src/components/fleet-hero-animated.tsx +383 -0
  44. package/src/components/input-error.tsx +17 -0
  45. package/src/components/keenicons/assets/duotone/Read Me.txt +7 -0
  46. package/src/components/keenicons/assets/duotone/demo-files/demo.css +160 -0
  47. package/src/components/keenicons/assets/duotone/demo-files/demo.js +32 -0
  48. package/src/components/keenicons/assets/duotone/demo.html +12424 -0
  49. package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.svg +1109 -0
  50. package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.ttf +0 -0
  51. package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.woff +0 -0
  52. package/src/components/keenicons/assets/duotone/selection.json +17313 -0
  53. package/src/components/keenicons/assets/duotone/style.css +4931 -0
  54. package/src/components/keenicons/assets/filled/Read Me.txt +7 -0
  55. package/src/components/keenicons/assets/filled/demo-files/demo.css +160 -0
  56. package/src/components/keenicons/assets/filled/demo-files/demo.js +32 -0
  57. package/src/components/keenicons/assets/filled/demo.html +12370 -0
  58. package/src/components/keenicons/assets/filled/fonts/keenicons-filled.svg +1082 -0
  59. package/src/components/keenicons/assets/filled/fonts/keenicons-filled.ttf +0 -0
  60. package/src/components/keenicons/assets/filled/fonts/keenicons-filled.woff +0 -0
  61. package/src/components/keenicons/assets/filled/selection.json +17096 -0
  62. package/src/components/keenicons/assets/filled/style.css +4769 -0
  63. package/src/components/keenicons/assets/outline/Read Me.txt +7 -0
  64. package/src/components/keenicons/assets/outline/demo-files/demo.css +160 -0
  65. package/src/components/keenicons/assets/outline/demo-files/demo.js +32 -0
  66. package/src/components/keenicons/assets/outline/demo.html +11356 -0
  67. package/src/components/keenicons/assets/outline/fonts/keenicons-outline.svg +575 -0
  68. package/src/components/keenicons/assets/outline/fonts/keenicons-outline.ttf +0 -0
  69. package/src/components/keenicons/assets/outline/fonts/keenicons-outline.woff +0 -0
  70. package/src/components/keenicons/assets/outline/selection.json +13054 -0
  71. package/src/components/keenicons/assets/outline/style.css +1721 -0
  72. package/src/components/keenicons/assets/solid/Read Me.txt +7 -0
  73. package/src/components/keenicons/assets/solid/demo-files/demo.css +160 -0
  74. package/src/components/keenicons/assets/solid/demo-files/demo.js +32 -0
  75. package/src/components/keenicons/assets/solid/demo.html +11356 -0
  76. package/src/components/keenicons/assets/solid/fonts/keenicons-solid.svg +575 -0
  77. package/src/components/keenicons/assets/solid/fonts/keenicons-solid.ttf +0 -0
  78. package/src/components/keenicons/assets/solid/fonts/keenicons-solid.woff +0 -0
  79. package/src/components/keenicons/assets/solid/selection.json +13048 -0
  80. package/src/components/keenicons/assets/solid/style.css +1721 -0
  81. package/src/components/keenicons/assets/styles.css +4 -0
  82. package/src/components/keenicons/index.ts +2 -0
  83. package/src/components/keenicons/keenicons.tsx +16 -0
  84. package/src/components/keenicons/types.ts +7 -0
  85. package/src/components/nav-footer.tsx +49 -0
  86. package/src/components/nav-main.tsx +53 -0
  87. package/src/components/nav-user.tsx +59 -0
  88. package/src/components/notification-bell.tsx +190 -0
  89. package/src/components/products/product-card.tsx +159 -0
  90. package/src/components/text-link.tsx +23 -0
  91. package/src/components/ui/accordion-menu.tsx +322 -0
  92. package/src/components/ui/accordion.tsx +133 -0
  93. package/src/components/ui/alert-dialog.tsx +82 -0
  94. package/src/components/ui/alert.tsx +63 -0
  95. package/src/components/ui/avatar-group.tsx +129 -0
  96. package/src/components/ui/avatar.tsx +67 -0
  97. package/src/components/ui/badge.tsx +230 -0
  98. package/src/components/ui/breadcrumb.tsx +88 -0
  99. package/src/components/ui/button.tsx +412 -0
  100. package/src/components/ui/calendar.tsx +56 -0
  101. package/src/components/ui/card.tsx +147 -0
  102. package/src/components/ui/chart.tsx +290 -0
  103. package/src/components/ui/checkbox.tsx +47 -0
  104. package/src/components/ui/code.tsx +45 -0
  105. package/src/components/ui/collapsible.tsx +31 -0
  106. package/src/components/ui/command-palette.tsx +189 -0
  107. package/src/components/ui/command.tsx +138 -0
  108. package/src/components/ui/cookie-banner.tsx +220 -0
  109. package/src/components/ui/copy-button.tsx +60 -0
  110. package/src/components/ui/data-grid-column-filter.tsx +124 -0
  111. package/src/components/ui/data-grid-column-header.tsx +284 -0
  112. package/src/components/ui/data-grid-column-visibility.tsx +38 -0
  113. package/src/components/ui/data-grid-pagination.tsx +206 -0
  114. package/src/components/ui/data-grid-table-dnd-rows.tsx +147 -0
  115. package/src/components/ui/data-grid-table-dnd.tsx +175 -0
  116. package/src/components/ui/data-grid-table.tsx +500 -0
  117. package/src/components/ui/data-grid.tsx +193 -0
  118. package/src/components/ui/data-list.tsx +76 -0
  119. package/src/components/ui/datefield.tsx +91 -0
  120. package/src/components/ui/dialog.tsx +139 -0
  121. package/src/components/ui/divider.tsx +41 -0
  122. package/src/components/ui/drawer.tsx +59 -0
  123. package/src/components/ui/dropdown-menu.tsx +224 -0
  124. package/src/components/ui/empty-state.tsx +54 -0
  125. package/src/components/ui/file-upload.tsx +152 -0
  126. package/src/components/ui/form.tsx +88 -0
  127. package/src/components/ui/icon.tsx +14 -0
  128. package/src/components/ui/input-otp.tsx +71 -0
  129. package/src/components/ui/input.tsx +155 -0
  130. package/src/components/ui/kbd.tsx +26 -0
  131. package/src/components/ui/label.tsx +31 -0
  132. package/src/components/ui/navigation-menu.tsx +168 -0
  133. package/src/components/ui/pagination.tsx +37 -0
  134. package/src/components/ui/placeholder-pattern.tsx +21 -0
  135. package/src/components/ui/popover.tsx +50 -0
  136. package/src/components/ui/progress.tsx +65 -0
  137. package/src/components/ui/radio-group.tsx +73 -0
  138. package/src/components/ui/resizable.tsx +39 -0
  139. package/src/components/ui/scroll-area.tsx +50 -0
  140. package/src/components/ui/select.tsx +234 -0
  141. package/src/components/ui/separator.tsx +24 -0
  142. package/src/components/ui/sheet.tsx +147 -0
  143. package/src/components/ui/sidebar.tsx +721 -0
  144. package/src/components/ui/skeleton.tsx +15 -0
  145. package/src/components/ui/slider.tsx +35 -0
  146. package/src/components/ui/sonner.tsx +28 -0
  147. package/src/components/ui/sortable.tsx +724 -0
  148. package/src/components/ui/spinner.tsx +17 -0
  149. package/src/components/ui/stat-card.tsx +82 -0
  150. package/src/components/ui/stepper.tsx +410 -0
  151. package/src/components/ui/switch.tsx +68 -0
  152. package/src/components/ui/table.tsx +42 -0
  153. package/src/components/ui/tabs.tsx +196 -0
  154. package/src/components/ui/timeline.tsx +90 -0
  155. package/src/components/ui/toggle-group.tsx +73 -0
  156. package/src/components/ui/toggle.tsx +45 -0
  157. package/src/components/ui/tooltip.tsx +55 -0
  158. package/src/components/user-info.tsx +33 -0
  159. package/src/components/user-menu-content.tsx +53 -0
  160. package/src/components/web/SiteFooter.tsx +154 -0
  161. package/src/components/web/SiteHeader.tsx +159 -0
  162. package/src/components/workflows/workflow-canvas.tsx +321 -0
  163. package/src/controls/Blockquote.tsx +25 -0
  164. package/src/controls/Button.tsx +101 -0
  165. package/src/controls/Checkbox.tsx +29 -0
  166. package/src/controls/DateField.tsx +37 -0
  167. package/src/controls/FormField.tsx +20 -0
  168. package/src/controls/Heading.tsx +28 -0
  169. package/src/controls/Input.tsx +21 -0
  170. package/src/controls/Label.tsx +18 -0
  171. package/src/controls/Paragraph.tsx +39 -0
  172. package/src/controls/PasswordInput.tsx +40 -0
  173. package/src/controls/RadioGroup.tsx +70 -0
  174. package/src/controls/Select.tsx +24 -0
  175. package/src/controls/Slider.tsx +33 -0
  176. package/src/controls/Switch.tsx +31 -0
  177. package/src/controls/Textarea.tsx +22 -0
  178. package/src/elements/ConfirmPasswordForm.tsx +43 -0
  179. package/src/elements/DeviceStatusBadge.tsx +38 -0
  180. package/src/elements/DriverCard.tsx +67 -0
  181. package/src/elements/ForgotPasswordForm.tsx +64 -0
  182. package/src/elements/IncidentCard.tsx +67 -0
  183. package/src/elements/LoginForm.tsx +100 -0
  184. package/src/elements/OtpForm.tsx +71 -0
  185. package/src/elements/RegisterForm.tsx +150 -0
  186. package/src/elements/ResetPasswordForm.tsx +72 -0
  187. package/src/elements/SmsChallengeForm.tsx +104 -0
  188. package/src/elements/VehicleCard.tsx +73 -0
  189. package/src/elements/VerifyEmailForm.tsx +39 -0
  190. package/src/hooks/use-appearance.tsx +117 -0
  191. package/src/hooks/use-applied-theme.ts +98 -0
  192. package/src/hooks/use-clipboard.ts +34 -0
  193. package/src/hooks/use-current-url.ts +83 -0
  194. package/src/hooks/use-dark-mode.ts +48 -0
  195. package/src/hooks/use-flash-toast.ts +29 -0
  196. package/src/hooks/use-initials.tsx +24 -0
  197. package/src/hooks/use-mobile-navigation.ts +12 -0
  198. package/src/hooks/use-mobile.tsx +38 -0
  199. package/src/index.ts +408 -0
  200. package/src/layouts/AppLayout.tsx +60 -0
  201. package/src/layouts/AuthLayout.tsx +32 -0
  202. package/src/layouts/SettingsLayout.tsx +21 -0
  203. package/src/layouts/app/AIChatLayout.tsx +73 -0
  204. package/src/layouts/app/AsideSidebarLayout.tsx +3 -0
  205. package/src/layouts/app/CalendarSidebarLayout.tsx +69 -0
  206. package/src/layouts/app/CommunitiesNavbarLayout.tsx +3 -0
  207. package/src/layouts/app/DualNavbarSidebarLayout.tsx +3 -0
  208. package/src/layouts/app/FocusSidebarLayout.tsx +75 -0
  209. package/src/layouts/app/MailLayout.tsx +69 -0
  210. package/src/layouts/app/MegaMenuHeaderLayout.tsx +3 -0
  211. package/src/layouts/app/MegaMenuLayout.tsx +81 -0
  212. package/src/layouts/app/MegaMenuNavbarLayout.tsx +88 -0
  213. package/src/layouts/app/MegaMenuSearchNavbarLayout.tsx +3 -0
  214. package/src/layouts/app/NavbarCollapsibleLayout.tsx +88 -0
  215. package/src/layouts/app/NavbarCollapsibleLinksLayout.tsx +3 -0
  216. package/src/layouts/app/NavbarMinimalLayout.tsx +3 -0
  217. package/src/layouts/app/NavbarMinimalSidebarLayout.tsx +3 -0
  218. package/src/layouts/app/NavbarSidebarDashboardLayout.tsx +3 -0
  219. package/src/layouts/app/NavbarSidebarLayout.tsx +92 -0
  220. package/src/layouts/app/NavbarSimpleSidebarLayout.tsx +3 -0
  221. package/src/layouts/app/NavbarTitledSidebarLayout.tsx +3 -0
  222. package/src/layouts/app/PanelSidebarLayout.tsx +3 -0
  223. package/src/layouts/app/SearchNavbarSidebarLayout.tsx +3 -0
  224. package/src/layouts/app/SidebarBreadcrumbLayout.tsx +3 -0
  225. package/src/layouts/app/SidebarCleanLayout.tsx +3 -0
  226. package/src/layouts/app/SidebarCommunitiesLayout.tsx +3 -0
  227. package/src/layouts/app/SidebarContentLayout.tsx +3 -0
  228. package/src/layouts/app/SidebarDualMenuLayout.tsx +104 -0
  229. package/src/layouts/app/SidebarFixedLayout.tsx +166 -0
  230. package/src/layouts/app/SidebarFooterNavbarLayout.tsx +3 -0
  231. package/src/layouts/app/SidebarHeaderMenuLayout.tsx +3 -0
  232. package/src/layouts/app/SidebarMegaMenuLayout.tsx +4 -0
  233. package/src/layouts/app/SidebarMinimalLayout.tsx +70 -0
  234. package/src/layouts/app/SidebarMobileSearchLayout.tsx +3 -0
  235. package/src/layouts/app/SidebarMultiPanelLayout.tsx +3 -0
  236. package/src/layouts/app/SidebarPrimarySecondaryLayout.tsx +3 -0
  237. package/src/layouts/app/SidebarSearchHeaderLayout.tsx +103 -0
  238. package/src/layouts/app/SidebarSearchToolbarLayout.tsx +3 -0
  239. package/src/layouts/app/SidebarTabsDualLayout.tsx +3 -0
  240. package/src/layouts/app/SidebarTabsLayout.tsx +98 -0
  241. package/src/layouts/app/SidebarTreeLayout.tsx +3 -0
  242. package/src/layouts/app/SplitNavbarLayout.tsx +3 -0
  243. package/src/layouts/app/SplitSidebarDashboardLayout.tsx +3 -0
  244. package/src/layouts/app/SplitSidebarLayout.tsx +99 -0
  245. package/src/layouts/app/TopNavLayout.tsx +105 -0
  246. package/src/layouts/app/TopNavLinksLayout.tsx +3 -0
  247. package/src/layouts/app/WorkspaceBreadcrumbLayout.tsx +3 -0
  248. package/src/layouts/app/WorkspaceCommunitiesLayout.tsx +3 -0
  249. package/src/layouts/app/WorkspaceNavbarLayout.tsx +3 -0
  250. package/src/layouts/app/WorkspaceSidebarLayout.tsx +98 -0
  251. package/src/layouts/app/WorkspaceSidebarTitleLayout.tsx +3 -0
  252. package/src/layouts/app/app-header-layout.tsx +45 -0
  253. package/src/layouts/app/app-sidebar-layout.tsx +56 -0
  254. package/src/layouts/app/layout-context.tsx +44 -0
  255. package/src/layouts/app/layout-types.ts +47 -0
  256. package/src/layouts/app/partials/Footer.tsx +35 -0
  257. package/src/layouts/app/partials/HeaderTopbar.tsx +96 -0
  258. package/src/layouts/app/partials/Navbar.tsx +85 -0
  259. package/src/layouts/app/partials/Toolbar.tsx +47 -0
  260. package/src/layouts/app-layout.tsx +29 -0
  261. package/src/layouts/auth/AuthBrandedLayout.tsx +58 -0
  262. package/src/layouts/auth/AuthCardLayout.tsx +31 -0
  263. package/src/layouts/auth/AuthCenteredLayout.tsx +41 -0
  264. package/src/layouts/auth/AuthClassicLayout.tsx +41 -0
  265. package/src/layouts/auth/AuthSimpleLayout.tsx +33 -0
  266. package/src/layouts/auth/AuthSplitLayout.tsx +89 -0
  267. package/src/layouts/web-app-layout.tsx +162 -0
  268. package/src/layouts/web-layout.tsx +23 -0
  269. package/src/lib/datetime.ts +188 -0
  270. package/src/lib/google-maps-loader.ts +99 -0
  271. package/src/lib/location.ts +127 -0
  272. package/src/lib/lucide-icon-map.ts +132 -0
  273. package/src/lib/map-markers.ts +124 -0
  274. package/src/lib/map-styles.ts +351 -0
  275. package/src/lib/utils.ts +11 -0
  276. package/src/platform/adapters/default.tsx +156 -0
  277. package/src/platform/adapters/inertia.tsx +88 -0
  278. package/src/platform/adapters/nextjs.ts +86 -0
  279. package/src/platform/context.tsx +106 -0
  280. package/src/platform/index.ts +27 -0
  281. package/src/platform/types.ts +105 -0
  282. package/src/styles/layouts/sidebar-fixed.css +161 -0
  283. package/src/styles/themes.css +583 -0
  284. package/src/types/assets.d.ts +5 -0
  285. package/src/types/auth.ts +25 -0
  286. package/src/types/global.d.ts +13 -0
  287. package/src/types/index.ts +9 -0
  288. package/src/types/navigation.ts +15 -0
  289. package/src/types/ui.ts +32 -0
@@ -0,0 +1,4 @@
1
+ @import './duotone/style.css';
2
+ @import './outline/style.css';
3
+ @import './filled/style.css';
4
+ @import './solid/style.css';
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './keenicons';
@@ -0,0 +1,16 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '../../lib/utils';
3
+ import type { KeeniconsProps } from './types';
4
+
5
+ export const KeenIcon = forwardRef<HTMLElement, KeeniconsProps>(
6
+ ({ icon, style = 'duotone', className = '', ...props }, ref) => {
7
+ return (
8
+ <i
9
+ ref={ref}
10
+ {...props}
11
+ className={cn(`ki-${style}`, `ki-${icon}`, className)}
12
+ />
13
+ );
14
+ },
15
+ );
16
+ KeenIcon.displayName = 'KeenIcon';
@@ -0,0 +1,7 @@
1
+ export type KeeniconsStyle = 'duotone' | 'filled' | 'solid' | 'outline';
2
+
3
+ export interface KeeniconsProps {
4
+ icon: string;
5
+ style?: KeeniconsStyle;
6
+ className?: string;
7
+ }
@@ -0,0 +1,49 @@
1
+ import type { ComponentPropsWithoutRef } from 'react';
2
+ import {
3
+ SidebarGroup,
4
+ SidebarGroupContent,
5
+ SidebarMenu,
6
+ SidebarMenuButton,
7
+ SidebarMenuItem,
8
+ } from './ui/sidebar';
9
+ import { toUrl } from '../lib/utils';
10
+ import type { NavItem } from '../types/navigation';
11
+
12
+ export function NavFooter({
13
+ items,
14
+ className,
15
+ ...props
16
+ }: ComponentPropsWithoutRef<typeof SidebarGroup> & {
17
+ items: NavItem[];
18
+ }) {
19
+ return (
20
+ <SidebarGroup
21
+ {...props}
22
+ className={`group-data-[collapsible=icon]:p-0 ${className || ''}`}
23
+ >
24
+ <SidebarGroupContent>
25
+ <SidebarMenu>
26
+ {items.map((item) => (
27
+ <SidebarMenuItem key={item.title}>
28
+ <SidebarMenuButton
29
+ asChild
30
+ className="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100"
31
+ >
32
+ <a
33
+ href={toUrl(item.href)}
34
+ target="_blank"
35
+ rel="noopener noreferrer"
36
+ >
37
+ {item.icon && (
38
+ <item.icon className="h-5 w-5" />
39
+ )}
40
+ <span>{item.title}</span>
41
+ </a>
42
+ </SidebarMenuButton>
43
+ </SidebarMenuItem>
44
+ ))}
45
+ </SidebarMenu>
46
+ </SidebarGroupContent>
47
+ </SidebarGroup>
48
+ );
49
+ }
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+ import { PlatformLink } from '../platform/context';
3
+ import {
4
+ SidebarGroup,
5
+ SidebarMenu,
6
+ SidebarMenuButton,
7
+ SidebarMenuItem,
8
+ } from './ui/sidebar';
9
+ import { useCurrentUrl } from '../hooks/use-current-url';
10
+ import type { NavItem } from '../types/navigation';
11
+
12
+ export function NavMain({ items = [] }: { items: NavItem[] }) {
13
+ const { isCurrentOrParentUrl } = useCurrentUrl();
14
+
15
+ return (
16
+ <SidebarGroup className="px-2 py-1">
17
+ <SidebarMenu className="gap-2">
18
+ {items.map((item) => {
19
+ const active = isCurrentOrParentUrl(item.href);
20
+
21
+ return (
22
+ <SidebarMenuItem key={item.title}>
23
+ <SidebarMenuButton
24
+ asChild
25
+ isActive={active}
26
+ size="lg"
27
+ tooltip={{ children: item.title }}
28
+ className={
29
+ active
30
+ ? 'border-l-[3px] border-sidebar-primary py-4 pl-[calc(0.75rem-3px)] text-sidebar-foreground'
31
+ : 'border-l-[3px] border-transparent py-4 pl-3 text-sidebar-foreground/70 hover:text-sidebar-foreground'
32
+ }
33
+ >
34
+ <PlatformLink
35
+ href={item.href}
36
+ prefetch
37
+ className="group-data-[collapsible=icon]:justify-center"
38
+ >
39
+ {item.icon && (
40
+ <item.icon className="h-[28px] w-[28px] shrink-0 group-data-[collapsible=icon]:mx-auto" />
41
+ )}
42
+ <span className="text-[13px] font-medium group-data-[collapsible=icon]:hidden">
43
+ {item.title}
44
+ </span>
45
+ </PlatformLink>
46
+ </SidebarMenuButton>
47
+ </SidebarMenuItem>
48
+ );
49
+ })}
50
+ </SidebarMenu>
51
+ </SidebarGroup>
52
+ );
53
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+ import { ChevronsUpDown } from 'lucide-react';
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuTrigger,
7
+ } from './ui/dropdown-menu';
8
+ import {
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ useSidebar,
13
+ } from './ui/sidebar';
14
+ import { UserInfo } from './user-info';
15
+ import { UserMenuContent } from './user-menu-content';
16
+ import { useIsMobile } from '../hooks/use-mobile';
17
+ import type { User } from '../types/auth';
18
+
19
+ export function NavUser({
20
+ user,
21
+ settingsUrl = '/settings/profile',
22
+ logoutUrl = '/logout',
23
+ }: {
24
+ user: User;
25
+ settingsUrl?: string;
26
+ logoutUrl?: string;
27
+ }) {
28
+ const { state } = useSidebar();
29
+ const isMobile = useIsMobile();
30
+
31
+ return (
32
+ <SidebarMenu>
33
+ <SidebarMenuItem>
34
+ <DropdownMenu>
35
+ <DropdownMenuTrigger asChild>
36
+ <SidebarMenuButton
37
+ size="lg"
38
+ className="group text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent"
39
+ >
40
+ <UserInfo user={user} />
41
+ <ChevronsUpDown className="ml-auto size-4" />
42
+ </SidebarMenuButton>
43
+ </DropdownMenuTrigger>
44
+ <DropdownMenuContent
45
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
46
+ align="end"
47
+ side={isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'}
48
+ >
49
+ <UserMenuContent
50
+ user={user}
51
+ settingsUrl={settingsUrl}
52
+ logoutUrl={logoutUrl}
53
+ />
54
+ </DropdownMenuContent>
55
+ </DropdownMenu>
56
+ </SidebarMenuItem>
57
+ </SidebarMenu>
58
+ );
59
+ }
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import { Bell, CheckCheck, ExternalLink } from 'lucide-react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { Button } from './ui/button';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuTrigger,
10
+ } from './ui/dropdown-menu';
11
+
12
+ interface NotificationData {
13
+ incident_id: number;
14
+ event_label: string;
15
+ priority_label: string;
16
+ message: string;
17
+ url: string;
18
+ }
19
+
20
+ interface Notification {
21
+ id: string;
22
+ data: NotificationData;
23
+ read_at: string | null;
24
+ created_at: string;
25
+ }
26
+
27
+ export interface NotificationBellProps {
28
+ unreadCount?: number;
29
+ fetchUrl?: string;
30
+ markReadUrl?: (id: string) => string;
31
+ markAllReadUrl?: string;
32
+ viewAllUrl?: string;
33
+ }
34
+
35
+ export function NotificationBell({
36
+ unreadCount = 0,
37
+ fetchUrl = '/api/v1/notifications',
38
+ markReadUrl = (id) => `/api/v1/notifications/${id}/read`,
39
+ markAllReadUrl = '/api/v1/notifications/read-all',
40
+ viewAllUrl = '/incidents',
41
+ }: NotificationBellProps) {
42
+ const [notifications, setNotifications] = useState<Notification[]>([]);
43
+ const [loading, setLoading] = useState(false);
44
+ const [open, setOpen] = useState(false);
45
+ const [localCount, setLocalCount] = useState(unreadCount);
46
+ const fetchedRef = useRef(false);
47
+
48
+ useEffect(() => { setLocalCount(unreadCount); }, [unreadCount]);
49
+
50
+ const fetchNotifications = useCallback(async () => {
51
+ if (loading) return;
52
+ setLoading(true);
53
+ try {
54
+ const res = await fetch(fetchUrl, {
55
+ headers: { Accept: 'application/json' },
56
+ credentials: 'same-origin',
57
+ });
58
+ if (res.ok) {
59
+ const json = await res.json();
60
+ setNotifications(json.notifications);
61
+ }
62
+ } finally {
63
+ setLoading(false);
64
+ fetchedRef.current = true;
65
+ }
66
+ }, [loading, fetchUrl]);
67
+
68
+ useEffect(() => {
69
+ if (open && !fetchedRef.current) fetchNotifications();
70
+ }, [open, fetchNotifications]);
71
+
72
+ const markRead = async (id: string) => {
73
+ await fetch(markReadUrl(id), {
74
+ method: 'POST',
75
+ headers: { 'X-CSRF-TOKEN': getCsrfToken(), Accept: 'application/json' },
76
+ credentials: 'same-origin',
77
+ });
78
+ setNotifications((prev) =>
79
+ prev.map((n) => n.id === id ? { ...n, read_at: new Date().toISOString() } : n),
80
+ );
81
+ setLocalCount((c) => Math.max(0, c - 1));
82
+ };
83
+
84
+ const markAllRead = async () => {
85
+ await fetch(markAllReadUrl, {
86
+ method: 'POST',
87
+ headers: { 'X-CSRF-TOKEN': getCsrfToken(), Accept: 'application/json' },
88
+ credentials: 'same-origin',
89
+ });
90
+ setNotifications((prev) => prev.map((n) => ({ ...n, read_at: new Date().toISOString() })));
91
+ setLocalCount(0);
92
+ };
93
+
94
+ const navigateTo = (notification: Notification) => {
95
+ if (!notification.read_at) markRead(notification.id);
96
+ setOpen(false);
97
+ window.location.href = notification.data.url;
98
+ };
99
+
100
+ return (
101
+ <DropdownMenu open={open} onOpenChange={setOpen}>
102
+ <DropdownMenuTrigger asChild>
103
+ <Button
104
+ variant="ghost"
105
+ size="icon"
106
+ className="relative h-9 w-9"
107
+ aria-label={`Notifications${localCount > 0 ? `, ${localCount} unread` : ''}`}
108
+ >
109
+ <Bell className="h-5 w-5" />
110
+ {localCount > 0 && (
111
+ <span className="absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-red-600 px-1 text-[10px] leading-none font-bold text-white">
112
+ {localCount > 99 ? '99+' : localCount}
113
+ </span>
114
+ )}
115
+ </Button>
116
+ </DropdownMenuTrigger>
117
+ <DropdownMenuContent align="end" className="w-80 p-0" sideOffset={8}>
118
+ <div className="flex items-center justify-between border-b px-4 py-3">
119
+ <span className="text-sm font-semibold">Notifications</span>
120
+ {localCount > 0 && (
121
+ <button
122
+ onClick={markAllRead}
123
+ className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
124
+ >
125
+ <CheckCheck className="h-3.5 w-3.5" />
126
+ Mark all read
127
+ </button>
128
+ )}
129
+ </div>
130
+
131
+ <div className="max-h-96 overflow-y-auto">
132
+ {loading && (
133
+ <div className="space-y-2 p-3">
134
+ {[1, 2, 3].map((i) => (
135
+ <div key={i} className="h-14 animate-pulse rounded bg-neutral-100" />
136
+ ))}
137
+ </div>
138
+ )}
139
+ {!loading && notifications.length === 0 && (
140
+ <div className="py-8 text-center text-sm text-neutral-500">No notifications</div>
141
+ )}
142
+ {!loading && notifications.map((notification) => (
143
+ <button
144
+ key={notification.id}
145
+ onClick={() => navigateTo(notification)}
146
+ className={`flex w-full items-start gap-3 border-b border-neutral-100 px-4 py-3 text-left text-sm last:border-0 hover:bg-neutral-50 ${!notification.read_at ? 'bg-blue-50' : ''}`}
147
+ >
148
+ <div className="mt-0.5 min-w-0 flex-1">
149
+ <p className={`truncate ${!notification.read_at ? 'font-medium' : 'text-neutral-700'}`}>
150
+ {notification.data.message}
151
+ </p>
152
+ <p className="mt-0.5 text-xs text-neutral-400">
153
+ {formatRelativeTime(notification.created_at)}
154
+ </p>
155
+ </div>
156
+ <ExternalLink className="mt-0.5 h-3.5 w-3.5 shrink-0 text-neutral-400" />
157
+ </button>
158
+ ))}
159
+ </div>
160
+
161
+ {notifications.length > 0 && (
162
+ <div className="border-t px-4 py-2 text-center">
163
+ <button
164
+ onClick={() => { setOpen(false); window.location.href = viewAllUrl; }}
165
+ className="text-xs text-blue-600 hover:text-blue-800"
166
+ >
167
+ View all incidents
168
+ </button>
169
+ </div>
170
+ )}
171
+ </DropdownMenuContent>
172
+ </DropdownMenu>
173
+ );
174
+ }
175
+
176
+ function getCsrfToken(): string {
177
+ return (
178
+ (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? ''
179
+ );
180
+ }
181
+
182
+ function formatRelativeTime(isoString: string): string {
183
+ const diff = Date.now() - new Date(isoString).getTime();
184
+ const mins = Math.floor(diff / 60000);
185
+ if (mins < 1) return 'just now';
186
+ if (mins < 60) return `${mins}m ago`;
187
+ const hrs = Math.floor(mins / 60);
188
+ if (hrs < 24) return `${hrs}h ago`;
189
+ return `${Math.floor(hrs / 24)}d ago`;
190
+ }
@@ -0,0 +1,159 @@
1
+ import { PlatformLink } from '../../platform/context';
2
+ /**
3
+ * Unified product card.
4
+ *
5
+ * Used everywhere a product is displayed (featured carousel, products grid,
6
+ * filter page). Theme-aware (uses semantic tokens, no raw hex), supports
7
+ * USD/PKR pricing, badge labels, out-of-stock and bulk-quantity states.
8
+ */
9
+ import { ArrowRight, Package } from 'lucide-react';
10
+
11
+ import { Badge } from '../ui/badge';
12
+ import { Card } from '../ui/card';
13
+
14
+ export type Currency = 'USD' | 'PKR';
15
+
16
+ export type ProductCardData = {
17
+ id: number;
18
+ name: string;
19
+ description: string | null;
20
+ image: string | null;
21
+ slug: string;
22
+ price_usd: number | null;
23
+ price_pkr?: number | null;
24
+ min_quantity?: number;
25
+ quantity_multiple?: number;
26
+ bulk_quantity?: number | null;
27
+ quantity?: number;
28
+ badge_label: string | null;
29
+ badge_color: string | null;
30
+ };
31
+
32
+ type Props = {
33
+ product: ProductCardData;
34
+ currency?: Currency;
35
+ /** Where the "Order" / "View Details" CTA navigates to. */
36
+ actionHref?: string;
37
+ actionLabel?: string;
38
+ };
39
+
40
+ export function formatProductPrice(
41
+ p: ProductCardData,
42
+ currency: Currency,
43
+ ): string | null {
44
+ if (currency === 'PKR') {
45
+ if (p.price_pkr != null) {
46
+ return `₨ ${p.price_pkr.toLocaleString('en-PK', { maximumFractionDigits: 0 })}`;
47
+ }
48
+
49
+ // Fall back to USD if PKR isn't priced — never show an empty cell.
50
+ return p.price_usd != null ? `$ ${p.price_usd.toFixed(2)}` : null;
51
+ }
52
+
53
+ return p.price_usd != null ? `$ ${p.price_usd.toFixed(2)}` : null;
54
+ }
55
+
56
+ export function ProductCard({
57
+ product,
58
+ currency = 'PKR',
59
+ actionHref,
60
+ actionLabel = 'View Details',
61
+ }: Props) {
62
+ const price = formatProductPrice(product, currency);
63
+ const href = actionHref ?? `/products/${product.slug}`;
64
+ const outOfStock = product.quantity === 0;
65
+ const isBulk =
66
+ product.bulk_quantity != null &&
67
+ product.quantity != null &&
68
+ product.quantity > product.bulk_quantity;
69
+
70
+ return (
71
+ <Card className="group flex h-full flex-col overflow-hidden border-border bg-card p-0 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md">
72
+ {/* Image */}
73
+ <div className="relative flex aspect-square items-center justify-center overflow-hidden bg-muted">
74
+ {product.image ? (
75
+ <img
76
+ src={product.image}
77
+ alt={product.name}
78
+ className="h-full w-full object-contain p-6 transition-transform duration-300 group-hover:scale-105"
79
+ />
80
+ ) : (
81
+ <div className="relative flex h-20 w-20 items-center justify-center rounded-2xl bg-background/60 shadow-sm">
82
+ <Package className="h-10 w-10 text-muted-foreground" />
83
+ </div>
84
+ )}
85
+ {product.badge_label && (
86
+ <Badge
87
+ className="absolute top-3 left-3"
88
+ style={
89
+ product.badge_color
90
+ ? {
91
+ backgroundColor: product.badge_color,
92
+ color: 'white',
93
+ }
94
+ : undefined
95
+ }
96
+ >
97
+ {product.badge_label}
98
+ </Badge>
99
+ )}
100
+ {outOfStock && (
101
+ <span className="absolute top-3 right-3 rounded-full bg-foreground/75 px-2.5 py-1 text-[11px] font-semibold text-background">
102
+ Out of Stock
103
+ </span>
104
+ )}
105
+ </div>
106
+
107
+ {/* Details */}
108
+ <div className="flex flex-1 flex-col p-5">
109
+ <h3 className="mb-1 text-base font-semibold text-card-foreground">
110
+ {product.name}
111
+ </h3>
112
+ {product.description && (
113
+ <p className="mb-4 line-clamp-2 flex-1 text-sm leading-relaxed text-muted-foreground">
114
+ {product.description}
115
+ </p>
116
+ )}
117
+
118
+ <div className="mt-auto flex items-center justify-between gap-3">
119
+ <div>
120
+ {price ? (
121
+ <span className="text-lg font-semibold text-foreground tabular-nums">
122
+ {price}
123
+ </span>
124
+ ) : (
125
+ <span className="text-sm text-muted-foreground italic">
126
+ Price on request
127
+ </span>
128
+ )}
129
+ {(product.min_quantity ?? 1) > 1 && (
130
+ <div className="text-xs text-muted-foreground">
131
+ Min {product.min_quantity}
132
+ {(product.quantity_multiple ?? 1) > 1
133
+ ? ` · ×${product.quantity_multiple}`
134
+ : ''}
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ {isBulk ? (
140
+ <a
141
+ href="/contact?topic=bulk"
142
+ className="inline-flex items-center gap-1.5 rounded-lg bg-warning-subtle px-3 py-1.5 text-xs font-semibold text-warning-fg transition hover:bg-warning-subtle/80"
143
+ >
144
+ Contact Us
145
+ </a>
146
+ ) : (
147
+ <a
148
+ href={href}
149
+ className="inline-flex items-center gap-1.5 rounded-lg bg-primary/10 px-3 py-1.5 text-sm font-semibold text-primary transition hover:bg-primary hover:text-primary-foreground"
150
+ >
151
+ {actionLabel}
152
+ <ArrowRight className="h-3.5 w-3.5" />
153
+ </a>
154
+ )}
155
+ </div>
156
+ </div>
157
+ </Card>
158
+ );
159
+ }
@@ -0,0 +1,23 @@
1
+ import { PlatformLink } from '../platform/context';
2
+ import type { ComponentProps } from 'react';
3
+ import { cn } from '../lib/utils';
4
+
5
+ type Props = ComponentProps<typeof PlatformLink>;
6
+
7
+ export default function TextLink({
8
+ className = '',
9
+ children,
10
+ ...props
11
+ }: Props) {
12
+ return (
13
+ <PlatformLink
14
+ className={cn(
15
+ 'text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500',
16
+ className,
17
+ )}
18
+ {...props}
19
+ >
20
+ {children}
21
+ </PlatformLink>
22
+ );
23
+ }