lytx 0.3.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 (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,45 @@
1
+ import { useTheme } from "@/app/providers/ThemeProvider";
2
+ import { chartColors, createChartTheme } from "@/app/utils/chartThemes";
3
+ import { getEventTypesDistribution, TableComponentProps } from "@db/tranformReports";
4
+ import DashboardCard from "../DashboardCard";
5
+ import { ResponsiveFunnel } from "@nivo/funnel";
6
+
7
+ export const EventTypesFunnel = ({
8
+ tableId,
9
+ tableData,
10
+ title,
11
+ labelsMap,
12
+ }: TableComponentProps & { labelsMap?: Map<string, string> }) => {
13
+ const displayTitle = title || tableData?.title || "Event Types";
14
+ const rows = tableData?.rows || [];
15
+ const { theme } = useTheme();
16
+ const chartTheme = createChartTheme(theme === "dark");
17
+ const funnelData = getEventTypesDistribution(rows, labelsMap);
18
+
19
+ const isEmpty = !tableData || rows.length === 0 || funnelData.length === 0;
20
+
21
+ return (
22
+ <DashboardCard id={tableId} title={displayTitle} className="mb-6" empty={isEmpty}>
23
+ <div className="h-90 w-full">
24
+ <ResponsiveFunnel
25
+ data={funnelData}
26
+ margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
27
+ valueFormat={(v) => `${v}%`}
28
+ colors={chartColors.funnel}
29
+ interpolation="smooth"
30
+ shapeBlending={0.9}
31
+ borderWidth={10}
32
+ borderOpacity={0.6}
33
+ labelColor={theme === "dark" ? "#ffffff" : "#111827"}
34
+ beforeSeparatorLength={90}
35
+ beforeSeparatorOffset={16}
36
+ afterSeparatorLength={90}
37
+ afterSeparatorOffset={16}
38
+ currentPartSizeExtension={8}
39
+ currentBorderWidth={20}
40
+ theme={chartTheme}
41
+ />
42
+ </div>
43
+ </DashboardCard>
44
+ );
45
+ };
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ import { DashboardCard } from "@components/DashboardCard";
4
+ import { Link } from "@components/ui/Link";
5
+ import {
6
+ DashboardResponseData,
7
+ } from "@db/tranformReports";
8
+ type EventSummaryData = NonNullable<DashboardResponseData["EventSummary"]>;
9
+ type EventSummaryRow = {
10
+ event: string | null;
11
+ count: number;
12
+ firstSeen: string | null;
13
+ lastSeen: string | null;
14
+ };
15
+ type EventSummaryRowWithPercent = EventSummaryRow & { share: number };
16
+ const formatEventShare = (share: number) => {
17
+ if (!Number.isFinite(share) || share <= 0) return "0%";
18
+ if (share < 1) return "<1%";
19
+ return `${share.toFixed(0)}%`;
20
+ };
21
+ const formatEventDate = (value: string | null, timezone: string) => {
22
+ if (!value) return "—";
23
+ const date = new Date(value);
24
+ if (Number.isNaN(date.getTime())) return "—";
25
+ try {
26
+ return date.toLocaleString(undefined, {
27
+ timeZone: timezone,
28
+ month: "numeric",
29
+ day: "numeric",
30
+ year: "numeric",
31
+ hour: "numeric",
32
+ minute: "2-digit",
33
+ });
34
+ } catch {
35
+ return date.toLocaleString(undefined, {
36
+ month: "numeric",
37
+ day: "numeric",
38
+ year: "numeric",
39
+ hour: "numeric",
40
+ minute: "2-digit",
41
+ });
42
+ }
43
+ };
44
+
45
+ const getAutocaptureDisplayName = (eventName: string): string => {
46
+ if (!eventName.startsWith("$ac_")) return eventName;
47
+
48
+ const parts = eventName.split("_");
49
+ const elementText = parts[2] || "unnamed";
50
+ const elementId = parts[3] || null;
51
+
52
+ return elementId ? `${elementText}_${elementId}` : elementText;
53
+ };
54
+ export const EventSummaryTable = ({
55
+ data,
56
+ isLoading,
57
+ timezone,
58
+ labelsMap,
59
+ }: {
60
+ data: EventSummaryData | null | undefined;
61
+ isLoading: boolean;
62
+ timezone: string;
63
+ labelsMap?: Map<string, string>;
64
+ }) => {
65
+ const summary = data?.summary ?? [];
66
+ const totalEvents = data?.totalEvents ?? 0;
67
+ const rows = summary.map((row): EventSummaryRowWithPercent => ({
68
+ ...row,
69
+ share: totalEvents > 0 ? (row.count / totalEvents) * 100 : 0,
70
+ }));
71
+
72
+ const eventCountSummary = (
73
+ <div className="flex items-center gap-3 text-sm text-(--theme-text-secondary)">
74
+ <span>
75
+ <span className="font-semibold text-(--theme-text-primary)">
76
+ {totalEvents.toLocaleString()}
77
+ </span>{" "}
78
+ total events •{" "}
79
+ <span className="font-semibold text-(--theme-text-primary)">
80
+ {(data?.totalEventTypes ?? 0).toLocaleString()}
81
+ </span>{" "}
82
+ event types
83
+ </span>
84
+ <Link
85
+ href="/dashboard/events"
86
+ className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
87
+ >
88
+ View all
89
+ <svg
90
+ width="14"
91
+ height="14"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ strokeWidth="2"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ aria-hidden="true"
99
+ >
100
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
101
+ <polyline points="15 3 21 3 21 9" />
102
+ <line x1="10" y1="14" x2="21" y2="3" />
103
+ </svg>
104
+ </Link>
105
+ </div>
106
+ );
107
+
108
+ return (
109
+ <DashboardCard
110
+ title="Events Overview"
111
+ titleAs="h3"
112
+ subtitle="All captured events with rollups by name."
113
+ actions={eventCountSummary}
114
+ isUpdating={isLoading}
115
+ updatingLabel="Updating events..."
116
+ empty={rows.length === 0}
117
+ emptyState={
118
+ <div className="rounded-md border border-(--theme-border-primary) bg-(--theme-bg-secondary) p-6 text-center">
119
+ <p className="text-sm text-(--theme-text-secondary)">
120
+ No events captured for this date range.
121
+ </p>
122
+ </div>
123
+ }
124
+ >
125
+ <div className="relative w-full">
126
+ <div className="overflow-y-auto overflow-x-auto scrollbar-none max-h-80">
127
+ <table className="min-w-180 w-full divide-y divide-(--theme-border-primary)">
128
+ <thead className="bg-(--theme-bg-secondary) sticky top-0 z-10">
129
+ <tr>
130
+ <th
131
+ scope="col"
132
+ className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-(--theme-text-secondary) uppercase tracking-wider"
133
+ >
134
+ Event
135
+ </th>
136
+ <th
137
+ scope="col"
138
+ className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-(--theme-text-secondary) uppercase tracking-wider"
139
+ >
140
+ Count
141
+ </th>
142
+ <th
143
+ scope="col"
144
+ className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-(--theme-text-secondary) uppercase tracking-wider"
145
+ >
146
+ Share
147
+ </th>
148
+ <th
149
+ scope="col"
150
+ className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-(--theme-text-secondary) uppercase tracking-wider"
151
+ >
152
+ First Seen
153
+ </th>
154
+ <th
155
+ scope="col"
156
+ className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-(--theme-text-secondary) uppercase tracking-wider"
157
+ >
158
+ Last Seen
159
+ </th>
160
+ </tr>
161
+ </thead>
162
+ <tbody className="bg-(--theme-card-bg) divide-y divide-(--theme-border-primary)">
163
+ {rows.map((row) => (
164
+ <tr
165
+ key={`${row.event ?? "unknown"}-${row.firstSeen ?? ""}-${row.lastSeen ?? ""}`}
166
+ className="hover:bg-(--theme-bg-secondary) transition-colors"
167
+ >
168
+ <td className="px-3 sm:px-6 py-4 text-sm text-(--theme-text-primary)">
169
+ {row.event
170
+ ? (labelsMap?.get(row.event) || getAutocaptureDisplayName(row.event))
171
+ : "Unknown"}
172
+ </td>
173
+ <td className="px-3 sm:px-6 py-4 text-sm text-(--theme-text-primary)">
174
+ {row.count.toLocaleString()}
175
+ </td>
176
+ <td className="px-3 sm:px-6 py-4 text-sm text-(--theme-text-primary)">
177
+ {formatEventShare(row.share)}
178
+ </td>
179
+ <td className="px-3 sm:px-6 py-4 text-sm text-(--theme-text-primary)">
180
+ {formatEventDate(row.firstSeen, timezone)}
181
+ </td>
182
+ <td className="px-3 sm:px-6 py-4 text-sm text-(--theme-text-primary)">
183
+ {formatEventDate(row.lastSeen, timezone)}
184
+ </td>
185
+ </tr>
186
+ ))}
187
+ </tbody>
188
+ </table>
189
+ </div>
190
+ <div className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-linear-to-l from-(--theme-card-bg) to-transparent sm:hidden" />
191
+ </div>
192
+ </DashboardCard>
193
+ );
194
+ }
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { ResponsiveSankey } from "@nivo/sankey";
4
+ import { DashboardCard } from "@components/DashboardCard";
5
+ import { useTheme } from "@/app/providers/ThemeProvider";
6
+
7
+ import { createChartTheme, chartColors } from "@/app/utils/chartThemes";
8
+ import { useMediaQuery } from "@/app/utils/media";
9
+ import {
10
+ type EventTypeDistributionItem,
11
+ type TableComponentProps,
12
+ getEventTypesDistribution,
13
+ } from "@db/tranformReports";
14
+
15
+ type SankeyFlowsProps = TableComponentProps & { labelsMap?: Map<string, string> };
16
+
17
+
18
+ export const SankeyFlows = ({
19
+ tableId,
20
+ tableData,
21
+ title,
22
+ labelsMap,
23
+ }: SankeyFlowsProps) => {
24
+ const displayTitle = title || "Event Types Sankey";
25
+ const rows = tableData?.rows || [];
26
+ const { theme } = useTheme();
27
+ const chartTheme = createChartTheme(theme === "dark");
28
+ const isSmallScreen = useMediaQuery("(max-width: 640px)");
29
+ const sankeyItems = getEventTypesDistribution(rows, labelsMap);
30
+
31
+ const nodes = [{ id: "All Events" }, ...sankeyItems.map((item: EventTypeDistributionItem) => ({ id: item.label }))];
32
+ const links = sankeyItems.map((item: EventTypeDistributionItem) => ({
33
+ source: "All Events",
34
+ target: item.label,
35
+ value: item.value,
36
+ }));
37
+
38
+ const isEmpty = !tableData || rows.length === 0 || sankeyItems.length === 0;
39
+
40
+ return (
41
+ <DashboardCard id={tableId} title={displayTitle} className="mb-6" empty={isEmpty}>
42
+ <div className="h-90 w-full">
43
+ <ResponsiveSankey
44
+ data={{ nodes, links }}
45
+ margin={
46
+ isSmallScreen
47
+ ? { top: 20, right: 100, bottom: 20, left: 76 }
48
+ : { top: 20, right: 180, bottom: 20, left: 120 }
49
+ }
50
+ align="justify"
51
+ colors={chartColors.funnel}
52
+ nodeOpacity={1}
53
+ nodeThickness={isSmallScreen ? 14 : 18}
54
+ nodeSpacing={isSmallScreen ? 18 : 24}
55
+ nodeBorderWidth={1}
56
+ nodeBorderColor={{ from: "color", modifiers: [["darker", 0.3]] }}
57
+ linkOpacity={0.45}
58
+ linkHoverOthersOpacity={0.1}
59
+ enableLinkGradient={true}
60
+ labelPosition="outside"
61
+ labelOrientation="horizontal"
62
+ labelPadding={12}
63
+ labelTextColor={theme === "dark" ? "#ffffff" : "#111827"}
64
+ valueFormat={(v) => `${v}%`}
65
+ theme={chartTheme}
66
+ />
67
+ </div>
68
+ </DashboardCard>
69
+ );
70
+ };
71
+
72
+ export default SankeyFlows;
@@ -0,0 +1,16 @@
1
+ export function CheckIcon({ className }: { className?: string }) {
2
+ return (
3
+ <svg
4
+ className={className}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ viewBox="0 0 24 24"
7
+ fill="currentColor"
8
+ >
9
+ <path
10
+ fillRule="evenodd"
11
+ d="M19.916 4.626a.75.75 0 01.208 1.04l-9 13.5a.75.75 0 01-1.154.114l-6-6a.75.75 0 011.06-1.06l5.353 5.353 8.493-12.739a.75.75 0 011.04-.208z"
12
+ clipRule="evenodd"
13
+ />
14
+ </svg>
15
+ );
16
+ }
@@ -0,0 +1,23 @@
1
+ import { MarketingFooter } from "@/app/components/MarketingFooter";
2
+ import { MarketingNav } from "@/app/components/MarketingNav";
3
+ import { ThemeProvider } from "@/app/providers/ThemeProvider";
4
+
5
+ export function MarketingLayout({
6
+ children,
7
+ navLinks,
8
+ navCta,
9
+ }: {
10
+ children: React.ReactNode;
11
+ navLinks?: { label: string; href: string }[];
12
+ navCta?: { label: string; href: string };
13
+ }) {
14
+ return (
15
+ <ThemeProvider>
16
+ <div className="min-h-screen bg-slate-50 text-slate-900 font-sans selection:bg-amber-100 selection:text-amber-900 dark:bg-black dark:text-slate-100">
17
+ <MarketingNav links={navLinks} cta={navCta} />
18
+ {children}
19
+ <MarketingFooter />
20
+ </div>
21
+ </ThemeProvider>
22
+ );
23
+ }
@@ -0,0 +1,35 @@
1
+ export function SectionHeading({
2
+ badge,
3
+ title,
4
+ subtitle,
5
+ align = "center",
6
+ as: Tag = "h2",
7
+ }: {
8
+ badge?: string;
9
+ title: string;
10
+ subtitle?: string;
11
+ align?: "left" | "center";
12
+ as?: "h1" | "h2";
13
+ }) {
14
+ return (
15
+ <div className={`mb-12 ${align === "center" ? "text-center" : "text-left"}`}>
16
+ {badge && (
17
+ <span className="inline-block py-1 px-3 rounded-full bg-amber-50 text-amber-700 text-xs font-semibold uppercase tracking-wider mb-4 border border-amber-100 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-800">
18
+ {badge}
19
+ </span>
20
+ )}
21
+ <Tag
22
+ className={`font-bold text-slate-900 dark:text-white mb-4 tracking-tight ${
23
+ Tag === "h1" ? "text-4xl md:text-5xl" : "text-3xl md:text-4xl"
24
+ }`}
25
+ >
26
+ {title}
27
+ </Tag>
28
+ {subtitle && (
29
+ <p className="text-lg text-slate-600 dark:text-slate-400 max-w-[46rem] mx-auto leading-relaxed">
30
+ {subtitle}
31
+ </p>
32
+ )}
33
+ </div>
34
+ );
35
+ }