@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,185 @@
|
|
|
1
|
+
import { PlatformLink } from '../../../platform/context';
|
|
2
|
+
import { ArrowRight } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { BlurredImage } from '../blurred-image';
|
|
5
|
+
import { Card } from '../../ui/card';
|
|
6
|
+
import { Icon } from '../../ui/icon';
|
|
7
|
+
import { lucideIcon } from '../../../lib/lucide-icon-map';
|
|
8
|
+
import { cn } from '../../../lib/utils';
|
|
9
|
+
|
|
10
|
+
export type CardItem = {
|
|
11
|
+
icon?: string | null;
|
|
12
|
+
image?: string | null;
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string | null;
|
|
15
|
+
value?: string | null;
|
|
16
|
+
link?: string | null;
|
|
17
|
+
link_label?: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CardsGridContent = {
|
|
21
|
+
eyebrow?: string | null;
|
|
22
|
+
title?: string | null;
|
|
23
|
+
subtitle?: string | null;
|
|
24
|
+
columns?: 2 | 3 | 4 | null;
|
|
25
|
+
card_style?: 'icon' | 'image' | 'stat' | 'minimal' | null;
|
|
26
|
+
items?: CardItem[] | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const COLS: Record<number, string> = {
|
|
30
|
+
2: 'sm:grid-cols-2',
|
|
31
|
+
3: 'sm:grid-cols-2 lg:grid-cols-3',
|
|
32
|
+
4: 'sm:grid-cols-2 lg:grid-cols-4',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generic card grid — the workhorse replacement for the old features /
|
|
37
|
+
* stats / store_benefits_strip section types. Four card styles:
|
|
38
|
+
*
|
|
39
|
+
* - icon : muted-tinted icon tile + title + description
|
|
40
|
+
* - image : product/asset photo via BlurredImage on top + title + body
|
|
41
|
+
* - stat : huge metric value + label + small icon (Lighthouse vibe)
|
|
42
|
+
* - minimal : compact text-only card for high-density grids
|
|
43
|
+
*/
|
|
44
|
+
export function CardsGridSection({
|
|
45
|
+
content,
|
|
46
|
+
identifier,
|
|
47
|
+
}: {
|
|
48
|
+
content?: CardsGridContent;
|
|
49
|
+
identifier?: string | null;
|
|
50
|
+
}) {
|
|
51
|
+
const columns = content?.columns ?? 3;
|
|
52
|
+
const style = content?.card_style ?? 'icon';
|
|
53
|
+
const items = content?.items ?? [];
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section
|
|
57
|
+
id={identifier ?? undefined}
|
|
58
|
+
className="bg-background py-20 sm:py-24"
|
|
59
|
+
>
|
|
60
|
+
<div className="mx-auto max-w-6xl px-6">
|
|
61
|
+
{(content?.eyebrow || content?.title || content?.subtitle) && (
|
|
62
|
+
<header className="mx-auto mb-12 max-w-2xl text-center">
|
|
63
|
+
{content?.eyebrow && (
|
|
64
|
+
<p className="mb-3 text-xs font-medium tracking-widest text-muted-foreground uppercase">
|
|
65
|
+
{content.eyebrow}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
{content?.title && (
|
|
69
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
|
70
|
+
{content.title}
|
|
71
|
+
</h2>
|
|
72
|
+
)}
|
|
73
|
+
{content?.subtitle && (
|
|
74
|
+
<p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
|
|
75
|
+
{content.subtitle}
|
|
76
|
+
</p>
|
|
77
|
+
)}
|
|
78
|
+
</header>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
<div
|
|
82
|
+
className={cn(
|
|
83
|
+
'grid grid-cols-1 gap-4',
|
|
84
|
+
COLS[columns] ?? COLS[3],
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{items.map((item, i) => (
|
|
88
|
+
<CardCell key={i} item={item} style={style} />
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CardCell({
|
|
97
|
+
item,
|
|
98
|
+
style,
|
|
99
|
+
}: {
|
|
100
|
+
item: CardItem;
|
|
101
|
+
style: 'icon' | 'image' | 'stat' | 'minimal';
|
|
102
|
+
}) {
|
|
103
|
+
const Icon$ = lucideIcon(item.icon);
|
|
104
|
+
|
|
105
|
+
const inner = (
|
|
106
|
+
<>
|
|
107
|
+
{style === 'icon' && Icon$ && (
|
|
108
|
+
<div className="mb-4 inline-flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
109
|
+
<Icon iconNode={Icon$} className="size-5" />
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{style === 'image' && (
|
|
114
|
+
<div className="-mx-2 -mt-2 mb-5 aspect-[4/3]">
|
|
115
|
+
<BlurredImage
|
|
116
|
+
src={item.image}
|
|
117
|
+
alt={item.title}
|
|
118
|
+
padding="p-4"
|
|
119
|
+
rounded="lg"
|
|
120
|
+
className="h-full w-full"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{style === 'stat' && (
|
|
126
|
+
<div className="mb-2 flex items-center gap-2">
|
|
127
|
+
{Icon$ && (
|
|
128
|
+
<Icon
|
|
129
|
+
iconNode={Icon$}
|
|
130
|
+
className="size-4 text-muted-foreground"
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
{item.value && (
|
|
134
|
+
<span className="text-3xl font-semibold tracking-tight text-primary sm:text-4xl">
|
|
135
|
+
{item.value}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
<h3
|
|
142
|
+
className={cn(
|
|
143
|
+
'text-card-foreground',
|
|
144
|
+
style === 'stat'
|
|
145
|
+
? 'text-sm font-medium text-muted-foreground'
|
|
146
|
+
: 'text-base font-semibold',
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
{item.title}
|
|
150
|
+
</h3>
|
|
151
|
+
|
|
152
|
+
{item.description && (
|
|
153
|
+
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
|
154
|
+
{item.description}
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{item.link && item.link_label && (
|
|
159
|
+
<span className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-primary">
|
|
160
|
+
{item.link_label}
|
|
161
|
+
<ArrowRight className="size-3.5" />
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
</>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const isMinimal = style === 'minimal';
|
|
168
|
+
const cardClasses = cn(
|
|
169
|
+
'h-full transition-colors',
|
|
170
|
+
isMinimal
|
|
171
|
+
? 'border-none bg-transparent p-4 shadow-none'
|
|
172
|
+
: 'border-border/60 bg-card p-6 hover:border-border',
|
|
173
|
+
item.link && 'cursor-pointer hover:border-primary/40',
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (item.link) {
|
|
177
|
+
return (
|
|
178
|
+
<PlatformLink href={item.link}>
|
|
179
|
+
<Card className={cardClasses}>{inner}</Card>
|
|
180
|
+
</PlatformLink>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return <Card className={cardClasses}>{inner}</Card>;
|
|
185
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CheckCircle2, Loader2, Mail, MapPin, Phone } from 'lucide-react';
|
|
4
|
+
import { useState, type FormEvent } from 'react';
|
|
5
|
+
import { Button } from '../../ui/button';
|
|
6
|
+
import { Card } from '../../ui/card';
|
|
7
|
+
import { Input } from '../../ui/input';
|
|
8
|
+
import { Label } from '../../ui/label';
|
|
9
|
+
import { cn } from '../../../lib/utils';
|
|
10
|
+
|
|
11
|
+
export type ContactFormContent = {
|
|
12
|
+
title?: string | null;
|
|
13
|
+
subtitle?: string | null;
|
|
14
|
+
fields?: ('name' | 'email' | 'phone' | 'company' | 'subject' | 'message')[] | null;
|
|
15
|
+
submit_label?: string | null;
|
|
16
|
+
success_message?: string | null;
|
|
17
|
+
contact_info?: {
|
|
18
|
+
phone?: string | null;
|
|
19
|
+
email?: string | null;
|
|
20
|
+
address?: string | null;
|
|
21
|
+
} | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type FieldName = 'name' | 'email' | 'phone' | 'company' | 'subject' | 'message';
|
|
25
|
+
|
|
26
|
+
interface ContactFormSectionProps {
|
|
27
|
+
content?: ContactFormContent;
|
|
28
|
+
identifier?: string | null;
|
|
29
|
+
endpoint?: string;
|
|
30
|
+
/** Pre-set flash state from the server (e.g. passed via Inertia shared props in the consuming app). */
|
|
31
|
+
flashSuccess?: boolean;
|
|
32
|
+
flashMessage?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ContactFormSection({ content, identifier, endpoint = '/contact', flashSuccess = false, flashMessage }: ContactFormSectionProps) {
|
|
36
|
+
const fields: FieldName[] = (content?.fields ?? ['name', 'email', 'phone', 'message']) as FieldName[];
|
|
37
|
+
const submitLabel = content?.submit_label ?? 'Send message';
|
|
38
|
+
const successCopy = content?.success_message ?? "Thanks — we'll reply within one business day.";
|
|
39
|
+
const info = content?.contact_info ?? {};
|
|
40
|
+
|
|
41
|
+
const [formData, setFormData] = useState<Record<FieldName, string>>({ name: '', email: '', phone: '', company: '', subject: '', message: '' });
|
|
42
|
+
const [errors, setErrors] = useState<Partial<Record<FieldName, string>>>({});
|
|
43
|
+
const [processing, setProcessing] = useState(false);
|
|
44
|
+
const [done, setDone] = useState(flashSuccess);
|
|
45
|
+
const [serverMessage, setServerMessage] = useState(flashMessage ?? '');
|
|
46
|
+
|
|
47
|
+
const set = (field: FieldName) => (v: string) => setFormData((prev) => ({ ...prev, [field]: v }));
|
|
48
|
+
|
|
49
|
+
async function onSubmit(e: FormEvent) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
setProcessing(true);
|
|
52
|
+
setErrors({});
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(endpoint, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
57
|
+
body: JSON.stringify(formData),
|
|
58
|
+
});
|
|
59
|
+
const data = await res.json().catch(() => ({}));
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
setErrors(data?.errors ?? {});
|
|
62
|
+
} else {
|
|
63
|
+
setDone(true);
|
|
64
|
+
setServerMessage(data?.message ?? successCopy);
|
|
65
|
+
setFormData({ name: '', email: '', phone: '', company: '', subject: '', message: '' });
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
setErrors({ message: 'Network error. Please try again.' });
|
|
69
|
+
} finally {
|
|
70
|
+
setProcessing(false);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<section id={identifier ?? undefined} className="bg-background py-20 sm:py-24">
|
|
76
|
+
<div className="mx-auto max-w-6xl px-6">
|
|
77
|
+
{(content?.title || content?.subtitle) && (
|
|
78
|
+
<header className="mx-auto mb-12 max-w-2xl text-center">
|
|
79
|
+
{content?.title && <h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">{content.title}</h2>}
|
|
80
|
+
{content?.subtitle && <p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">{content.subtitle}</p>}
|
|
81
|
+
</header>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<div className="grid gap-8 lg:grid-cols-3">
|
|
85
|
+
<Card className="border-border/60 bg-card p-6 lg:col-span-2">
|
|
86
|
+
{done ? (
|
|
87
|
+
<div className="flex flex-col items-start gap-3 rounded-lg bg-primary/5 p-6 text-primary">
|
|
88
|
+
<CheckCircle2 className="size-6" />
|
|
89
|
+
<p className="text-base font-medium">{serverMessage || successCopy}</p>
|
|
90
|
+
</div>
|
|
91
|
+
) : (
|
|
92
|
+
<form onSubmit={onSubmit} className="grid grid-cols-1 gap-4 sm:grid-cols-2" noValidate>
|
|
93
|
+
{fields.includes('name') && <Field id="name" label="Name" required value={formData.name} onChange={set('name')} error={errors.name} autoComplete="name" />}
|
|
94
|
+
{fields.includes('email') && <Field id="email" label="Email" required type="email" value={formData.email} onChange={set('email')} error={errors.email} autoComplete="email" />}
|
|
95
|
+
{fields.includes('phone') && <Field id="phone" label="Phone" type="tel" value={formData.phone} onChange={set('phone')} error={errors.phone} autoComplete="tel" />}
|
|
96
|
+
{fields.includes('company') && <Field id="company" label="Company" value={formData.company} onChange={set('company')} error={errors.company} autoComplete="organization" />}
|
|
97
|
+
{fields.includes('subject') && <Field id="subject" label="Subject" className="sm:col-span-2" value={formData.subject} onChange={set('subject')} error={errors.subject} />}
|
|
98
|
+
{fields.includes('message') && (
|
|
99
|
+
<div className="sm:col-span-2">
|
|
100
|
+
<Label htmlFor="message">Message<span className="text-destructive"> *</span></Label>
|
|
101
|
+
<textarea id="message" required rows={5} value={formData.message} onChange={(e) => set('message')(e.target.value)}
|
|
102
|
+
className={cn('mt-1.5 w-full rounded-md border bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50', errors.message ? 'border-destructive' : 'border-input')}
|
|
103
|
+
/>
|
|
104
|
+
{errors.message && <p className="mt-1 text-xs text-destructive">{errors.message}</p>}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
<div className="sm:col-span-2">
|
|
108
|
+
<Button type="submit" size="lg" disabled={processing} className="w-full sm:w-auto">
|
|
109
|
+
{processing && <Loader2 className="size-4 animate-spin" />}
|
|
110
|
+
{submitLabel}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</form>
|
|
114
|
+
)}
|
|
115
|
+
</Card>
|
|
116
|
+
|
|
117
|
+
{(info.phone || info.email || info.address) && (
|
|
118
|
+
<Card className="flex flex-col gap-5 border-border/60 bg-card p-6">
|
|
119
|
+
<h3 className="text-sm font-semibold tracking-tight">Get in touch</h3>
|
|
120
|
+
{info.phone && <ContactInfoRow icon={<Phone className="size-4 text-primary" />} label="Phone" value={info.phone} href={`tel:${info.phone.replace(/\s+/g, '')}`} />}
|
|
121
|
+
{info.email && <ContactInfoRow icon={<Mail className="size-4 text-primary" />} label="Email" value={info.email} href={`mailto:${info.email}`} />}
|
|
122
|
+
{info.address && <ContactInfoRow icon={<MapPin className="size-4 text-primary" />} label="Office" value={info.address} />}
|
|
123
|
+
</Card>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Field({ id, label, type = 'text', required, value, onChange, error, className, autoComplete }: {
|
|
132
|
+
id: string; label: string; type?: string; required?: boolean;
|
|
133
|
+
value: string; onChange: (v: string) => void; error?: string; className?: string; autoComplete?: string;
|
|
134
|
+
}) {
|
|
135
|
+
return (
|
|
136
|
+
<div className={className}>
|
|
137
|
+
<Label htmlFor={id}>{label}{required && <span className="text-destructive"> *</span>}</Label>
|
|
138
|
+
<Input id={id} type={type} required={required} value={value} onChange={(e) => onChange(e.target.value)} autoComplete={autoComplete}
|
|
139
|
+
className={cn('mt-1.5', error && 'border-destructive focus-visible:border-destructive')} />
|
|
140
|
+
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ContactInfoRow({ icon, label, value, href }: { icon: React.ReactNode; label: string; value: string; href?: string }) {
|
|
146
|
+
const content = (
|
|
147
|
+
<div className="flex items-start gap-3">
|
|
148
|
+
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-primary/10">{icon}</div>
|
|
149
|
+
<div className="min-w-0">
|
|
150
|
+
<p className="text-xs text-muted-foreground">{label}</p>
|
|
151
|
+
<p className="text-sm font-medium text-card-foreground">{value}</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
if (href) return <a href={href} className="block hover:opacity-80">{content}</a>;
|
|
156
|
+
return content;
|
|
157
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SectionBackground } from '../section-bg';
|
|
2
|
+
import type { CmsBackground } from '../section-bg';
|
|
3
|
+
import { SectionButtons } from '../section-button';
|
|
4
|
+
import type { CmsButton } from '../section-button';
|
|
5
|
+
import { cn } from '../../../lib/utils';
|
|
6
|
+
|
|
7
|
+
export type CtaContent = {
|
|
8
|
+
eyebrow?: string | null;
|
|
9
|
+
title?: string | null;
|
|
10
|
+
subtitle?: string | null;
|
|
11
|
+
alignment?: 'left' | 'center' | null;
|
|
12
|
+
bg?: CmsBackground | null;
|
|
13
|
+
buttons?: CmsButton[] | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Call-to-action section. Composed of: background (any bg{} mode except
|
|
18
|
+
* map), eyebrow, title, subtitle, and a buttons[] row. Common patterns:
|
|
19
|
+
* - tinted accent panel ("Bulk orders" / "Custom solutions")
|
|
20
|
+
* - bold primary-coloured rail at the end of a page
|
|
21
|
+
* - dark gradient closing band on the home page
|
|
22
|
+
*/
|
|
23
|
+
export function CtaSection({
|
|
24
|
+
content,
|
|
25
|
+
identifier,
|
|
26
|
+
}: {
|
|
27
|
+
content?: CtaContent;
|
|
28
|
+
identifier?: string | null;
|
|
29
|
+
}) {
|
|
30
|
+
const alignment = content?.alignment ?? 'center';
|
|
31
|
+
const bg = content?.bg ?? { kind: 'color', color_token: 'accent' };
|
|
32
|
+
const isDarkSurface = ['image', 'video', 'gradient'].includes(
|
|
33
|
+
bg.kind ?? '',
|
|
34
|
+
);
|
|
35
|
+
const isPrimaryColor =
|
|
36
|
+
bg.kind === 'color' && (bg.color_token ?? '') === 'primary';
|
|
37
|
+
const onDark = isDarkSurface || isPrimaryColor;
|
|
38
|
+
const textColor = onDark ? 'text-white' : 'text-foreground';
|
|
39
|
+
const supportColor = onDark ? 'text-white/80' : 'text-muted-foreground';
|
|
40
|
+
const eyebrowColor = onDark ? 'text-white/70' : 'text-muted-foreground';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<section
|
|
44
|
+
id={identifier ?? undefined}
|
|
45
|
+
className="relative isolate overflow-hidden"
|
|
46
|
+
>
|
|
47
|
+
<SectionBackground bg={bg} />
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
'relative mx-auto max-w-4xl px-6 py-16 sm:py-20',
|
|
52
|
+
alignment === 'center' ? 'text-center' : 'text-left',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{content?.eyebrow && (
|
|
56
|
+
<p
|
|
57
|
+
className={cn(
|
|
58
|
+
'mb-3 text-xs font-medium tracking-widest uppercase',
|
|
59
|
+
eyebrowColor,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{content.eyebrow}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{content?.title && (
|
|
67
|
+
<h2
|
|
68
|
+
className={cn(
|
|
69
|
+
'text-3xl font-semibold tracking-tight text-balance sm:text-4xl',
|
|
70
|
+
textColor,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{content.title}
|
|
74
|
+
</h2>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{content?.subtitle && (
|
|
78
|
+
<p
|
|
79
|
+
className={cn(
|
|
80
|
+
'mx-auto mt-4 max-w-2xl text-base leading-relaxed text-pretty sm:text-lg',
|
|
81
|
+
supportColor,
|
|
82
|
+
alignment === 'left' && 'mx-0',
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{content.subtitle}
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{content?.buttons && content.buttons.length > 0 && (
|
|
90
|
+
<div className="mt-8">
|
|
91
|
+
<SectionButtons
|
|
92
|
+
buttons={content.buttons}
|
|
93
|
+
align={alignment}
|
|
94
|
+
size="lg"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
);
|
|
101
|
+
}
|