@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,256 @@
|
|
|
1
|
+
import { PlatformLink } from '../../../platform/context';
|
|
2
|
+
import { ArrowRight, Calendar } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { BlurredImage } from '../blurred-image';
|
|
5
|
+
import { SectionButtons } from '../section-button';
|
|
6
|
+
import type { CmsButton } from '../section-button';
|
|
7
|
+
import { Badge } from '../../ui/badge';
|
|
8
|
+
import { cn } from '../../../lib/utils';
|
|
9
|
+
|
|
10
|
+
export type FeaturedBlogSliderContent = {
|
|
11
|
+
eyebrow?: string | null;
|
|
12
|
+
title?: string | null;
|
|
13
|
+
subtitle?: string | null;
|
|
14
|
+
featured_blog_slug?: string | null;
|
|
15
|
+
list_count?: number | null;
|
|
16
|
+
buttons?: CmsButton[] | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type BlogPostCard = {
|
|
20
|
+
id: number;
|
|
21
|
+
title: string;
|
|
22
|
+
slug: string;
|
|
23
|
+
excerpt: string | null;
|
|
24
|
+
cover_image: string | null;
|
|
25
|
+
published_at: string | null;
|
|
26
|
+
author: { name: string } | null;
|
|
27
|
+
tags: { name: string; slug: string; color: string | null }[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Featured blog slider — large hero post on the left + vertical
|
|
32
|
+
* scroll-snap list of recent posts on the right. The "featured" post
|
|
33
|
+
* is content.featured_blog_slug if set, otherwise the newest published
|
|
34
|
+
* post; the list contains up to `list_count` newer-first remaining posts.
|
|
35
|
+
*/
|
|
36
|
+
export function FeaturedBlogSliderSection({
|
|
37
|
+
content,
|
|
38
|
+
posts,
|
|
39
|
+
identifier,
|
|
40
|
+
}: {
|
|
41
|
+
content?: FeaturedBlogSliderContent;
|
|
42
|
+
posts?: BlogPostCard[];
|
|
43
|
+
identifier?: string | null;
|
|
44
|
+
}) {
|
|
45
|
+
const listCount = content?.list_count ?? 5;
|
|
46
|
+
const pool = posts ?? [];
|
|
47
|
+
|
|
48
|
+
if (pool.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const explicit = content?.featured_blog_slug
|
|
53
|
+
? pool.find((p) => p.slug === content.featured_blog_slug)
|
|
54
|
+
: null;
|
|
55
|
+
const featured = explicit ?? pool[0];
|
|
56
|
+
const list = pool.filter((p) => p.id !== featured.id).slice(0, listCount);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
id={identifier ?? undefined}
|
|
61
|
+
className="bg-background py-20 sm:py-24"
|
|
62
|
+
>
|
|
63
|
+
<div className="mx-auto max-w-6xl px-6">
|
|
64
|
+
{(content?.eyebrow || content?.title || content?.subtitle) && (
|
|
65
|
+
<header className="mb-10 max-w-2xl">
|
|
66
|
+
{content?.eyebrow && (
|
|
67
|
+
<p className="mb-3 text-xs font-medium tracking-widest text-muted-foreground uppercase">
|
|
68
|
+
{content.eyebrow}
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
{content?.title && (
|
|
72
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
|
73
|
+
{content.title}
|
|
74
|
+
</h2>
|
|
75
|
+
)}
|
|
76
|
+
{content?.subtitle && (
|
|
77
|
+
<p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
|
|
78
|
+
{content.subtitle}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</header>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<div className="grid gap-6 lg:grid-cols-12">
|
|
85
|
+
<FeaturedPost post={featured} />
|
|
86
|
+
<PostList posts={list} />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{content?.buttons && content.buttons.length > 0 && (
|
|
90
|
+
<div className="mt-10">
|
|
91
|
+
<SectionButtons
|
|
92
|
+
buttons={content.buttons}
|
|
93
|
+
align="left"
|
|
94
|
+
size="md"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function FeaturedPost({ post }: { post: BlogPostCard }) {
|
|
104
|
+
return (
|
|
105
|
+
<PlatformLink href={`/blog/${post.slug}`} className="group block lg:col-span-7">
|
|
106
|
+
<article className="flex h-full flex-col overflow-hidden rounded-2xl border border-border/60 bg-card transition-colors hover:border-primary/40">
|
|
107
|
+
<div className="relative aspect-[16/10] overflow-hidden bg-muted">
|
|
108
|
+
<BlurredImage
|
|
109
|
+
src={post.cover_image}
|
|
110
|
+
alt={post.title}
|
|
111
|
+
padding="p-0"
|
|
112
|
+
rounded="md"
|
|
113
|
+
fit="cover"
|
|
114
|
+
whiteWash={0}
|
|
115
|
+
className="h-full w-full transition-transform duration-500 group-hover:scale-[1.02]"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex flex-1 flex-col p-6">
|
|
119
|
+
<TagsRow tags={post.tags} />
|
|
120
|
+
<h3 className="mt-2 text-2xl font-semibold tracking-tight text-balance text-card-foreground sm:text-3xl">
|
|
121
|
+
{post.title}
|
|
122
|
+
</h3>
|
|
123
|
+
{post.excerpt && (
|
|
124
|
+
<p className="mt-3 line-clamp-3 flex-1 leading-relaxed text-pretty text-muted-foreground">
|
|
125
|
+
{post.excerpt}
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
<PostMeta post={post} className="mt-5" />
|
|
129
|
+
</div>
|
|
130
|
+
</article>
|
|
131
|
+
</PlatformLink>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function PostList({ posts }: { posts: BlogPostCard[] }) {
|
|
136
|
+
if (posts.length === 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
className={cn(
|
|
143
|
+
'lg:col-span-5',
|
|
144
|
+
// Vertical scroll-snap on tall screens; auto-height otherwise.
|
|
145
|
+
'lg:max-h-[640px] lg:snap-y lg:snap-mandatory lg:overflow-y-auto',
|
|
146
|
+
'lg:pr-2 lg:[scrollbar-width:thin]',
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
<ul className="flex flex-col gap-3">
|
|
150
|
+
{posts.map((p) => (
|
|
151
|
+
<li key={p.id} className="lg:snap-start">
|
|
152
|
+
<a
|
|
153
|
+
href={`/blog/${p.slug}`}
|
|
154
|
+
className="group flex items-start gap-4 rounded-xl border border-border/60 bg-card p-4 transition-colors hover:border-primary/40"
|
|
155
|
+
>
|
|
156
|
+
<div className="size-16 shrink-0 overflow-hidden rounded-md bg-muted">
|
|
157
|
+
<BlurredImage
|
|
158
|
+
src={p.cover_image}
|
|
159
|
+
alt=""
|
|
160
|
+
padding="p-0"
|
|
161
|
+
rounded="md"
|
|
162
|
+
fit="cover"
|
|
163
|
+
whiteWash={0}
|
|
164
|
+
className="h-full w-full"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
168
|
+
<TagsRow tags={p.tags.slice(0, 1)} />
|
|
169
|
+
<h4 className="mt-1 line-clamp-2 text-sm font-semibold text-card-foreground group-hover:text-primary">
|
|
170
|
+
{p.title}
|
|
171
|
+
</h4>
|
|
172
|
+
{p.published_at && (
|
|
173
|
+
<time
|
|
174
|
+
dateTime={p.published_at}
|
|
175
|
+
className="mt-1 text-xs text-muted-foreground"
|
|
176
|
+
>
|
|
177
|
+
{formatDate(p.published_at)}
|
|
178
|
+
</time>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
<ArrowRight className="mt-1 size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-primary" />
|
|
182
|
+
</a>
|
|
183
|
+
</li>
|
|
184
|
+
))}
|
|
185
|
+
</ul>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function TagsRow({ tags }: { tags: BlogPostCard['tags'] }) {
|
|
191
|
+
if (!tags || tags.length === 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="flex flex-wrap gap-1.5">
|
|
197
|
+
{tags.map((t) => (
|
|
198
|
+
<Badge
|
|
199
|
+
key={t.slug}
|
|
200
|
+
variant="secondary"
|
|
201
|
+
className="text-[10px] font-medium"
|
|
202
|
+
style={
|
|
203
|
+
t.color
|
|
204
|
+
? {
|
|
205
|
+
backgroundColor: `${t.color}1a`,
|
|
206
|
+
color: t.color,
|
|
207
|
+
}
|
|
208
|
+
: undefined
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
{t.name}
|
|
212
|
+
</Badge>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function PostMeta({
|
|
219
|
+
post,
|
|
220
|
+
className,
|
|
221
|
+
}: {
|
|
222
|
+
post: BlogPostCard;
|
|
223
|
+
className?: string;
|
|
224
|
+
}) {
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
className={cn(
|
|
228
|
+
'flex items-center gap-3 text-xs text-muted-foreground',
|
|
229
|
+
className,
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{post.author && <span>{post.author.name}</span>}
|
|
233
|
+
{post.author && post.published_at && <span>·</span>}
|
|
234
|
+
{post.published_at && (
|
|
235
|
+
<span className="inline-flex items-center gap-1">
|
|
236
|
+
<Calendar className="size-3" />
|
|
237
|
+
<time dateTime={post.published_at}>
|
|
238
|
+
{formatDate(post.published_at)}
|
|
239
|
+
</time>
|
|
240
|
+
</span>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatDate(iso: string): string {
|
|
247
|
+
try {
|
|
248
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
249
|
+
month: 'short',
|
|
250
|
+
day: 'numeric',
|
|
251
|
+
year: 'numeric',
|
|
252
|
+
});
|
|
253
|
+
} catch {
|
|
254
|
+
return iso;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { SectionButtons } from '../section-button';
|
|
7
|
+
import type { CmsButton } from '../section-button';
|
|
8
|
+
import { ProductCard } from '../../products/product-card';
|
|
9
|
+
import type { ProductCardData } from '../../products/product-card';
|
|
10
|
+
import { Button } from '../../ui/button';
|
|
11
|
+
import { cn } from '../../../lib/utils';
|
|
12
|
+
|
|
13
|
+
export type FeaturedProductsGridContent = {
|
|
14
|
+
eyebrow?: string | null;
|
|
15
|
+
title?: string | null;
|
|
16
|
+
subtitle?: string | null;
|
|
17
|
+
columns?: 2 | 3 | 4 | null;
|
|
18
|
+
max_items?: number | null;
|
|
19
|
+
show_buttons_on_cards?: boolean | null;
|
|
20
|
+
card_button_label?: string | null;
|
|
21
|
+
buttons?: CmsButton[] | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ProductCard = ProductCardData;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Featured products carousel. Pulls from `featured_products` shared prop
|
|
28
|
+
* (DeviceType::featured()). Renders a horizontal scroll-snap row with
|
|
29
|
+
* arrow controls — same `ProductCard` component used everywhere else,
|
|
30
|
+
* so cards stay visually consistent across surfaces.
|
|
31
|
+
*/
|
|
32
|
+
export function FeaturedProductsGridSection({
|
|
33
|
+
content,
|
|
34
|
+
products,
|
|
35
|
+
identifier,
|
|
36
|
+
}: {
|
|
37
|
+
content?: FeaturedProductsGridContent;
|
|
38
|
+
products?: ProductCardData[];
|
|
39
|
+
identifier?: string | null;
|
|
40
|
+
}) {
|
|
41
|
+
const max = content?.max_items ?? 8;
|
|
42
|
+
const items = (products ?? []).slice(0, max > 0 ? max : undefined);
|
|
43
|
+
|
|
44
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
46
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const el = scrollerRef.current;
|
|
50
|
+
|
|
51
|
+
if (!el) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const update = () => {
|
|
56
|
+
setCanScrollLeft(el.scrollLeft > 4);
|
|
57
|
+
setCanScrollRight(
|
|
58
|
+
el.scrollLeft + el.clientWidth < el.scrollWidth - 4,
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
update();
|
|
62
|
+
el.addEventListener('scroll', update, { passive: true });
|
|
63
|
+
window.addEventListener('resize', update);
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
el.removeEventListener('scroll', update);
|
|
67
|
+
window.removeEventListener('resize', update);
|
|
68
|
+
};
|
|
69
|
+
}, [items.length]);
|
|
70
|
+
|
|
71
|
+
function scrollBy(direction: 1 | -1) {
|
|
72
|
+
const el = scrollerRef.current;
|
|
73
|
+
|
|
74
|
+
if (!el) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const card = el.querySelector<HTMLElement>('[data-product-card]');
|
|
79
|
+
const step = card ? card.offsetWidth + 20 : el.clientWidth * 0.8;
|
|
80
|
+
el.scrollBy({ left: direction * step, behavior: 'smooth' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (items.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<section
|
|
89
|
+
id={identifier ?? undefined}
|
|
90
|
+
className="bg-muted/30 py-20 sm:py-24"
|
|
91
|
+
>
|
|
92
|
+
<div className="mx-auto max-w-7xl px-6">
|
|
93
|
+
{(content?.eyebrow || content?.title || content?.subtitle) && (
|
|
94
|
+
<header className="mx-auto mb-10 max-w-2xl text-center">
|
|
95
|
+
{content?.eyebrow && (
|
|
96
|
+
<p className="mb-3 text-xs font-medium tracking-widest text-muted-foreground uppercase">
|
|
97
|
+
{content.eyebrow}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
{content?.title && (
|
|
101
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
|
102
|
+
{content.title}
|
|
103
|
+
</h2>
|
|
104
|
+
)}
|
|
105
|
+
{content?.subtitle && (
|
|
106
|
+
<p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
|
|
107
|
+
{content.subtitle}
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
</header>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
<div className="relative">
|
|
114
|
+
{/* Arrow controls */}
|
|
115
|
+
<Button
|
|
116
|
+
type="button"
|
|
117
|
+
variant="outline"
|
|
118
|
+
size="icon"
|
|
119
|
+
aria-label="Previous"
|
|
120
|
+
onClick={() => scrollBy(-1)}
|
|
121
|
+
disabled={!canScrollLeft}
|
|
122
|
+
className={cn(
|
|
123
|
+
'absolute top-1/2 -left-2 z-10 hidden -translate-y-1/2 rounded-full bg-background shadow-md sm:flex',
|
|
124
|
+
!canScrollLeft && 'opacity-0',
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<ChevronLeft className="size-5" />
|
|
128
|
+
</Button>
|
|
129
|
+
<Button
|
|
130
|
+
type="button"
|
|
131
|
+
variant="outline"
|
|
132
|
+
size="icon"
|
|
133
|
+
aria-label="Next"
|
|
134
|
+
onClick={() => scrollBy(1)}
|
|
135
|
+
disabled={!canScrollRight}
|
|
136
|
+
className={cn(
|
|
137
|
+
'absolute top-1/2 -right-2 z-10 hidden -translate-y-1/2 rounded-full bg-background shadow-md sm:flex',
|
|
138
|
+
!canScrollRight && 'opacity-0',
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
<ChevronRight className="size-5" />
|
|
142
|
+
</Button>
|
|
143
|
+
|
|
144
|
+
{/* Scroll-snap carousel */}
|
|
145
|
+
<div
|
|
146
|
+
ref={scrollerRef}
|
|
147
|
+
className="flex snap-x snap-mandatory gap-5 overflow-x-auto pb-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
148
|
+
>
|
|
149
|
+
{items.map((p) => (
|
|
150
|
+
<div
|
|
151
|
+
key={p.id}
|
|
152
|
+
data-product-card
|
|
153
|
+
className="w-[260px] shrink-0 snap-start sm:w-[280px] lg:w-[300px]"
|
|
154
|
+
>
|
|
155
|
+
<ProductCard product={p} />
|
|
156
|
+
</div>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{content?.buttons && content.buttons.length > 0 && (
|
|
162
|
+
<div className="mt-12">
|
|
163
|
+
<SectionButtons
|
|
164
|
+
buttons={content.buttons}
|
|
165
|
+
align="center"
|
|
166
|
+
size="lg"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</section>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { PlatformLink } from '../../../platform/context';
|
|
2
|
+
import { ArrowRight } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { SectionButtons } from '../section-button';
|
|
5
|
+
import type { CmsButton } from '../section-button';
|
|
6
|
+
import { Card } from '../../ui/card';
|
|
7
|
+
import { Icon } from '../../ui/icon';
|
|
8
|
+
import { lucideIcon } from '../../../lib/lucide-icon-map';
|
|
9
|
+
import { cn } from '../../../lib/utils';
|
|
10
|
+
|
|
11
|
+
export type FeaturedSolutionsGridContent = {
|
|
12
|
+
eyebrow?: string | null;
|
|
13
|
+
title?: string | null;
|
|
14
|
+
subtitle?: string | null;
|
|
15
|
+
columns?: 2 | 3 | 4 | null;
|
|
16
|
+
max_items?: number | null;
|
|
17
|
+
show_buttons_on_cards?: boolean | null;
|
|
18
|
+
card_button_label?: string | null;
|
|
19
|
+
buttons?: CmsButton[] | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SolutionCard = {
|
|
23
|
+
id: number;
|
|
24
|
+
title: string;
|
|
25
|
+
description: string | null;
|
|
26
|
+
icon_name: string | null;
|
|
27
|
+
gradient_from?: string | null;
|
|
28
|
+
gradient_to?: string | null;
|
|
29
|
+
cta_label?: string | null;
|
|
30
|
+
cta_href?: string | null;
|
|
31
|
+
slug?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const COLS: Record<number, string> = {
|
|
35
|
+
2: 'sm:grid-cols-2',
|
|
36
|
+
3: 'sm:grid-cols-2 lg:grid-cols-3',
|
|
37
|
+
4: 'sm:grid-cols-2 lg:grid-cols-4',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Featured solutions grid. Pulls from `solutions` shared prop
|
|
42
|
+
* (Solution::featured()). Each card renders icon + title + description
|
|
43
|
+
* with an optional per-card CTA, plus an optional row of section-level
|
|
44
|
+
* buttons below the grid.
|
|
45
|
+
*/
|
|
46
|
+
export function FeaturedSolutionsGridSection({
|
|
47
|
+
content,
|
|
48
|
+
solutions,
|
|
49
|
+
identifier,
|
|
50
|
+
}: {
|
|
51
|
+
content?: FeaturedSolutionsGridContent;
|
|
52
|
+
solutions?: SolutionCard[];
|
|
53
|
+
identifier?: string | null;
|
|
54
|
+
}) {
|
|
55
|
+
const columns = content?.columns ?? 3;
|
|
56
|
+
const max = content?.max_items ?? 0;
|
|
57
|
+
const showCta = content?.show_buttons_on_cards ?? true;
|
|
58
|
+
const ctaLabel = content?.card_button_label ?? 'Learn more';
|
|
59
|
+
|
|
60
|
+
const items = (solutions ?? []).slice(0, max > 0 ? max : undefined);
|
|
61
|
+
|
|
62
|
+
if (items.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section
|
|
68
|
+
id={identifier ?? undefined}
|
|
69
|
+
className="bg-background py-20 sm:py-24"
|
|
70
|
+
>
|
|
71
|
+
<div className="mx-auto max-w-6xl px-6">
|
|
72
|
+
{(content?.eyebrow || content?.title || content?.subtitle) && (
|
|
73
|
+
<header className="mx-auto mb-12 max-w-2xl text-center">
|
|
74
|
+
{content?.eyebrow && (
|
|
75
|
+
<p className="mb-3 text-xs font-medium tracking-widest text-muted-foreground uppercase">
|
|
76
|
+
{content.eyebrow}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
{content?.title && (
|
|
80
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
|
81
|
+
{content.title}
|
|
82
|
+
</h2>
|
|
83
|
+
)}
|
|
84
|
+
{content?.subtitle && (
|
|
85
|
+
<p className="mt-3 text-base leading-relaxed text-pretty text-muted-foreground sm:text-lg">
|
|
86
|
+
{content.subtitle}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
</header>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
'grid grid-cols-1 gap-4',
|
|
95
|
+
COLS[columns] ?? COLS[3],
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{items.map((s) => (
|
|
99
|
+
<SolutionCardCell
|
|
100
|
+
key={s.id}
|
|
101
|
+
solution={s}
|
|
102
|
+
showCta={showCta}
|
|
103
|
+
ctaLabel={ctaLabel}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{content?.buttons && content.buttons.length > 0 && (
|
|
109
|
+
<div className="mt-12">
|
|
110
|
+
<SectionButtons
|
|
111
|
+
buttons={content.buttons}
|
|
112
|
+
align="center"
|
|
113
|
+
size="lg"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</section>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function SolutionCardCell({
|
|
123
|
+
solution,
|
|
124
|
+
showCta,
|
|
125
|
+
ctaLabel,
|
|
126
|
+
}: {
|
|
127
|
+
solution: SolutionCard;
|
|
128
|
+
showCta: boolean;
|
|
129
|
+
ctaLabel: string;
|
|
130
|
+
}) {
|
|
131
|
+
const Icon$ = lucideIcon(solution.icon_name);
|
|
132
|
+
const href =
|
|
133
|
+
solution.cta_href ??
|
|
134
|
+
(solution.slug ? `/solutions/${solution.slug}` : null);
|
|
135
|
+
|
|
136
|
+
const card = (
|
|
137
|
+
<Card className="group flex h-full flex-col border-border/60 bg-card p-6 transition-colors hover:border-primary/40">
|
|
138
|
+
{Icon$ && (
|
|
139
|
+
<div
|
|
140
|
+
className={cn(
|
|
141
|
+
'mb-4 inline-flex size-12 items-center justify-center rounded-xl text-white',
|
|
142
|
+
// Use the brand gradient if both stops set; otherwise default to primary.
|
|
143
|
+
solution.gradient_from && solution.gradient_to
|
|
144
|
+
? 'bg-gradient-to-br'
|
|
145
|
+
: 'bg-primary',
|
|
146
|
+
)}
|
|
147
|
+
style={
|
|
148
|
+
solution.gradient_from && solution.gradient_to
|
|
149
|
+
? {
|
|
150
|
+
backgroundImage: `linear-gradient(to bottom right, var(--color-${solution.gradient_from}, #2563eb), var(--color-${solution.gradient_to}, #1d4ed8))`,
|
|
151
|
+
}
|
|
152
|
+
: undefined
|
|
153
|
+
}
|
|
154
|
+
>
|
|
155
|
+
<Icon iconNode={Icon$} className="size-5" />
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
<h3 className="text-lg font-semibold text-card-foreground">
|
|
160
|
+
{solution.title}
|
|
161
|
+
</h3>
|
|
162
|
+
|
|
163
|
+
{solution.description && (
|
|
164
|
+
<p className="mt-2 line-clamp-3 flex-1 text-sm leading-relaxed text-muted-foreground">
|
|
165
|
+
{solution.description}
|
|
166
|
+
</p>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{showCta && href && (
|
|
170
|
+
<div className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-primary group-hover:gap-2">
|
|
171
|
+
{solution.cta_label ?? ctaLabel}
|
|
172
|
+
<ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</Card>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (href) {
|
|
179
|
+
return <PlatformLink href={href}>{card}</PlatformLink>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return card;
|
|
183
|
+
}
|