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.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const chartAccentOrange = "#f97316";
|
|
2
|
+
|
|
3
|
+
export const createChartTheme = (isDark: boolean) => ({
|
|
4
|
+
background: "transparent",
|
|
5
|
+
text: {
|
|
6
|
+
fontSize: 12,
|
|
7
|
+
fill: isDark ? "#ffffff" : "#111827",
|
|
8
|
+
fontWeight: 600,
|
|
9
|
+
outlineWidth: 0,
|
|
10
|
+
outlineColor: "transparent",
|
|
11
|
+
},
|
|
12
|
+
axis: {
|
|
13
|
+
domain: {
|
|
14
|
+
line: {
|
|
15
|
+
stroke: isDark ? "#575353" : "#E5E7EB",
|
|
16
|
+
strokeWidth: 1,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
legend: {
|
|
20
|
+
text: {
|
|
21
|
+
fontSize: 12,
|
|
22
|
+
fill: isDark ? "#ffffff" : "#6B7280",
|
|
23
|
+
fontWeight: 600,
|
|
24
|
+
outlineWidth: 0,
|
|
25
|
+
outlineColor: "transparent",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
ticks: {
|
|
29
|
+
line: {
|
|
30
|
+
stroke: isDark ? "#575353" : "#E5E7EB",
|
|
31
|
+
strokeWidth: 1,
|
|
32
|
+
},
|
|
33
|
+
text: {
|
|
34
|
+
fontSize: 11,
|
|
35
|
+
fill: isDark ? "#ffffff" : "#6B7280",
|
|
36
|
+
fontWeight: 600,
|
|
37
|
+
outlineWidth: 0,
|
|
38
|
+
outlineColor: "transparent",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
grid: {
|
|
43
|
+
line: {
|
|
44
|
+
stroke: isDark ? "#575353" : "#F3F4F6",
|
|
45
|
+
strokeWidth: 1,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
legends: {
|
|
49
|
+
title: {
|
|
50
|
+
text: {
|
|
51
|
+
fontSize: 11,
|
|
52
|
+
fill: isDark ? "#ffffff" : "#6B7280",
|
|
53
|
+
fontWeight: 600,
|
|
54
|
+
outlineWidth: 0,
|
|
55
|
+
outlineColor: "transparent",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
text: {
|
|
59
|
+
fontSize: 11,
|
|
60
|
+
fill: isDark ? "#ffffff" : "#6B7280",
|
|
61
|
+
fontWeight: 600,
|
|
62
|
+
outlineWidth: 0,
|
|
63
|
+
outlineColor: "transparent",
|
|
64
|
+
},
|
|
65
|
+
ticks: {
|
|
66
|
+
line: {},
|
|
67
|
+
text: {
|
|
68
|
+
fontSize: 10,
|
|
69
|
+
fill: isDark ? "#ffffff" : "#6B7280",
|
|
70
|
+
fontWeight: 600,
|
|
71
|
+
outlineWidth: 0,
|
|
72
|
+
outlineColor: "transparent",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
labels: {
|
|
77
|
+
text: {
|
|
78
|
+
fontSize: 12,
|
|
79
|
+
fill: isDark ? "#ffffff" : "#111827",
|
|
80
|
+
fontWeight: 600,
|
|
81
|
+
outlineWidth: 0,
|
|
82
|
+
outlineColor: "transparent",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
annotations: {
|
|
86
|
+
text: {
|
|
87
|
+
fontSize: 13,
|
|
88
|
+
fill: isDark ? "#ffffff" : "#111827",
|
|
89
|
+
fontWeight: 600,
|
|
90
|
+
outlineWidth: 2,
|
|
91
|
+
outlineColor: isDark ? "#3c3c3c" : "#FFFFFF",
|
|
92
|
+
outlineOpacity: 1,
|
|
93
|
+
},
|
|
94
|
+
link: {
|
|
95
|
+
stroke: isDark ? "#575353" : "#6B7280",
|
|
96
|
+
strokeWidth: 1,
|
|
97
|
+
outlineWidth: 2,
|
|
98
|
+
outlineColor: isDark ? "#3c3c3c" : "#FFFFFF",
|
|
99
|
+
outlineOpacity: 1,
|
|
100
|
+
},
|
|
101
|
+
outline: {
|
|
102
|
+
stroke: isDark ? "#575353" : "#6B7280",
|
|
103
|
+
strokeWidth: 2,
|
|
104
|
+
outlineWidth: 2,
|
|
105
|
+
outlineColor: isDark ? "#3c3c3c" : "#FFFFFF",
|
|
106
|
+
outlineOpacity: 1,
|
|
107
|
+
},
|
|
108
|
+
symbol: {
|
|
109
|
+
fill: isDark ? "#575353" : "#6B7280",
|
|
110
|
+
outlineWidth: 2,
|
|
111
|
+
outlineColor: isDark ? "#3c3c3c" : "#FFFFFF",
|
|
112
|
+
outlineOpacity: 1,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
tooltip: {
|
|
116
|
+
container: {
|
|
117
|
+
background: isDark ? "#484743" : "#FFFFFF",
|
|
118
|
+
color: isDark ? "#ffffff" : "#111827",
|
|
119
|
+
fontSize: 12,
|
|
120
|
+
fontWeight: 600,
|
|
121
|
+
borderRadius: "8px",
|
|
122
|
+
boxShadow: isDark
|
|
123
|
+
? "0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)"
|
|
124
|
+
: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
|
125
|
+
border: `1px solid ${isDark ? "#575353" : "#E5E7EB"}`,
|
|
126
|
+
},
|
|
127
|
+
basic: {},
|
|
128
|
+
chip: {},
|
|
129
|
+
table: {},
|
|
130
|
+
tableCell: {},
|
|
131
|
+
tableCellValue: {},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const chartColors = {
|
|
136
|
+
primary: ["#3B82F6", "#1D4ED8", "#1E40AF", "#1E3A8A", "#93C5FD"],
|
|
137
|
+
secondary: ["#10B981", "#34D399", "#6EE7B7", "#A7F3D0", "#D1FAE5"],
|
|
138
|
+
mixed: ["#3B82F6", "#10B981", chartAccentOrange, "#EF4444", "#1D4ED8", "#06B6D4"],
|
|
139
|
+
gradient: [
|
|
140
|
+
{ offset: 0, color: "#3B82F6" },
|
|
141
|
+
{ offset: 100, color: "#1D4ED8" },
|
|
142
|
+
],
|
|
143
|
+
map: ["#93C5FD", "#60A5FA", "#3B82F6", "#2563EB", "#1D4ED8"],
|
|
144
|
+
line: ["#3B82F6"],
|
|
145
|
+
funnel: ["#e05205", "#f06a1a", "#f97316", "#fb923c", "#fdba74"],
|
|
146
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single keybind entry: the key to listen for and the action to run.
|
|
5
|
+
* `key` is matched case-insensitively against `KeyboardEvent.key`.
|
|
6
|
+
*/
|
|
7
|
+
export interface Keybind {
|
|
8
|
+
/** The key value (e.g. "r", "H", "6", "0"). Matched case-insensitively. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** Callback fired when the key is pressed. */
|
|
11
|
+
action: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseKeybindsOptions {
|
|
15
|
+
/** The list of keybinds to register. */
|
|
16
|
+
binds: Keybind[];
|
|
17
|
+
/** Only listen when this is true (default: true). */
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Ignore key events originating from input/textarea/select elements
|
|
21
|
+
* so users can still type normally (default: true).
|
|
22
|
+
*/
|
|
23
|
+
ignoreInputs?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Ignore key events with modifier keys held (ctrl, alt, meta, shift).
|
|
26
|
+
* Prevents collisions with browser/system shortcuts (default: true).
|
|
27
|
+
*/
|
|
28
|
+
ignoreWithModifiers?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registers global keyboard shortcuts.
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* ```ts
|
|
36
|
+
* useKeybinds({
|
|
37
|
+
* binds: [
|
|
38
|
+
* { key: "r", action: () => selectPreset("Last 30 min") },
|
|
39
|
+
* { key: "w", action: () => selectPreset("Last 7 days") },
|
|
40
|
+
* ],
|
|
41
|
+
* enabled: isDatePickerOpen,
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useKeybinds(options: UseKeybindsOptions) {
|
|
46
|
+
const {
|
|
47
|
+
binds,
|
|
48
|
+
enabled = true,
|
|
49
|
+
ignoreInputs = true,
|
|
50
|
+
ignoreWithModifiers = true,
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
// Keep binds in a ref so the event handler always sees the latest list
|
|
54
|
+
// without needing to re-attach the listener on every render.
|
|
55
|
+
const bindsRef = useRef(binds);
|
|
56
|
+
bindsRef.current = binds;
|
|
57
|
+
|
|
58
|
+
const enabledRef = useRef(enabled);
|
|
59
|
+
enabledRef.current = enabled;
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
63
|
+
if (!enabledRef.current) return;
|
|
64
|
+
|
|
65
|
+
// Skip when a modifier is held (unless the bind IS a modifier, which we don't support).
|
|
66
|
+
if (ignoreWithModifiers && (event.ctrlKey || event.altKey || event.metaKey)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip when focus is inside a form control.
|
|
71
|
+
if (ignoreInputs) {
|
|
72
|
+
const tag = (event.target as HTMLElement)?.tagName;
|
|
73
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Also skip contenteditable elements.
|
|
77
|
+
if ((event.target as HTMLElement)?.isContentEditable) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const pressed = event.key.toLowerCase();
|
|
83
|
+
|
|
84
|
+
for (const bind of bindsRef.current) {
|
|
85
|
+
if (bind.key.toLowerCase() === pressed) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
bind.action();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
94
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
95
|
+
}, [ignoreInputs, ignoreWithModifiers]);
|
|
96
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useMediaQuery(query: string) {
|
|
4
|
+
const [matches, setMatches] = useState(false);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (typeof window === "undefined") return;
|
|
8
|
+
|
|
9
|
+
const media = window.matchMedia(query);
|
|
10
|
+
|
|
11
|
+
const listener = () => {
|
|
12
|
+
setMatches(media.matches);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
listener();
|
|
16
|
+
|
|
17
|
+
media.addEventListener("change", listener);
|
|
18
|
+
return () => {
|
|
19
|
+
media.removeEventListener("change", listener);
|
|
20
|
+
};
|
|
21
|
+
}, [query]);
|
|
22
|
+
|
|
23
|
+
return matches;
|
|
24
|
+
}
|
package/src/client.tsx
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initClient,
|
|
3
|
+
initClientNavigation,
|
|
4
|
+
} from "rwsdk/client";
|
|
5
|
+
|
|
6
|
+
type NavigationRuntime = {
|
|
7
|
+
handleResponse: ReturnType<typeof initClientNavigation>["handleResponse"];
|
|
8
|
+
onHydrated: () => void;
|
|
9
|
+
clickListener: (event: MouseEvent) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
declare global {
|
|
13
|
+
interface Window {
|
|
14
|
+
__lytxNavigationRuntime?: NavigationRuntime;
|
|
15
|
+
__lytxClientInitialized?: boolean;
|
|
16
|
+
__viewTransitionResolve?: (() => void) | null;
|
|
17
|
+
__viewTransitionTimeoutId?: number | null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const clearPendingViewTransition = () => {
|
|
22
|
+
if (window.__viewTransitionTimeoutId) {
|
|
23
|
+
window.clearTimeout(window.__viewTransitionTimeoutId);
|
|
24
|
+
window.__viewTransitionTimeoutId = null;
|
|
25
|
+
}
|
|
26
|
+
if (window.__viewTransitionResolve) {
|
|
27
|
+
window.__viewTransitionResolve();
|
|
28
|
+
window.__viewTransitionResolve = null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createNavigationRuntime = (): NavigationRuntime => {
|
|
33
|
+
const { handleResponse, onHydrated: originalOnHydrated } = initClientNavigation({
|
|
34
|
+
scrollBehavior: "instant",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const onHydrated = () => {
|
|
38
|
+
clearPendingViewTransition();
|
|
39
|
+
originalOnHydrated();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const clickListener = (event: MouseEvent) => {
|
|
43
|
+
const target = event.target as HTMLElement | null;
|
|
44
|
+
const link = target?.closest("a[href]") as HTMLAnchorElement | null;
|
|
45
|
+
|
|
46
|
+
if (!link) return;
|
|
47
|
+
if (link.target === "_blank" || link.hasAttribute("download")) return;
|
|
48
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
49
|
+
|
|
50
|
+
const href = link.getAttribute("href");
|
|
51
|
+
if (!href || href.startsWith("//") || href.startsWith("http")) return;
|
|
52
|
+
|
|
53
|
+
const nextUrl = new URL(link.href, window.location.href);
|
|
54
|
+
const currentUrl = new URL(window.location.href);
|
|
55
|
+
if (
|
|
56
|
+
nextUrl.origin !== currentUrl.origin
|
|
57
|
+
|| (nextUrl.pathname === currentUrl.pathname
|
|
58
|
+
&& nextUrl.search === currentUrl.search
|
|
59
|
+
&& nextUrl.hash === currentUrl.hash)
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const startViewTransition = (document as any).startViewTransition as
|
|
65
|
+
| ((callback: () => Promise<void>) => unknown)
|
|
66
|
+
| undefined;
|
|
67
|
+
if (!startViewTransition) return;
|
|
68
|
+
|
|
69
|
+
clearPendingViewTransition();
|
|
70
|
+
startViewTransition(() => {
|
|
71
|
+
return new Promise<void>((resolve) => {
|
|
72
|
+
window.__viewTransitionResolve = resolve;
|
|
73
|
+
window.__viewTransitionTimeoutId = window.setTimeout(() => {
|
|
74
|
+
clearPendingViewTransition();
|
|
75
|
+
}, 1200);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
document.addEventListener("click", clickListener, true);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
handleResponse,
|
|
84
|
+
onHydrated,
|
|
85
|
+
clickListener,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const runtime = window.__lytxNavigationRuntime ?? createNavigationRuntime();
|
|
90
|
+
window.__lytxNavigationRuntime = runtime;
|
|
91
|
+
|
|
92
|
+
if (!window.__lytxClientInitialized) {
|
|
93
|
+
initClient({ handleResponse: runtime.handleResponse, onHydrated: runtime.onHydrated });
|
|
94
|
+
window.__lytxClientInitialized = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (import.meta.hot) {
|
|
98
|
+
import.meta.hot.dispose(() => {
|
|
99
|
+
if (window.__lytxNavigationRuntime) {
|
|
100
|
+
document.removeEventListener("click", window.__lytxNavigationRuntime.clickListener, true);
|
|
101
|
+
}
|
|
102
|
+
window.__lytxNavigationRuntime = undefined;
|
|
103
|
+
window.__lytxClientInitialized = false;
|
|
104
|
+
clearPendingViewTransition();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (import.meta.env.PROD && "serviceWorker" in navigator) {
|
|
109
|
+
window.addEventListener("load", () => {
|
|
110
|
+
navigator.serviceWorker.register("/sw.js").catch((error) => {
|
|
111
|
+
console.error("Service worker registration failed:", error);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const CREATE_LYTX_APP_CONFIG_DOC_URL =
|
|
4
|
+
"https://github.com/lytx-io/kit/blob/master/core/docs/oss-contract.md#supported-extension-and-customization-points";
|
|
5
|
+
|
|
6
|
+
const dbAdapterValues = ["sqlite", "postgres", "singlestore", "analytics_engine"] as const;
|
|
7
|
+
|
|
8
|
+
const dbAdapterSchema = z.enum(dbAdapterValues);
|
|
9
|
+
const eventStoreSchema = z.enum([...dbAdapterValues, "durable_objects"] as const);
|
|
10
|
+
|
|
11
|
+
const dbConfigSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
dbAdapter: dbAdapterSchema.optional(),
|
|
14
|
+
eventStore: eventStoreSchema.optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
|
|
18
|
+
export type LytxDbAdapter = z.infer<typeof dbAdapterSchema>;
|
|
19
|
+
export type LytxEventStore = z.infer<typeof eventStoreSchema>;
|
|
20
|
+
export type LytxDbConfig = z.input<typeof dbConfigSchema>;
|
|
21
|
+
|
|
22
|
+
const routePathSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.trim()
|
|
25
|
+
.min(1, "Route path is required")
|
|
26
|
+
.refine((value) => value.startsWith("/"), "Route path must start with '/'")
|
|
27
|
+
.refine((value) => !/\s/.test(value), "Route path cannot contain whitespace");
|
|
28
|
+
|
|
29
|
+
const routePrefixSchema = z
|
|
30
|
+
.string()
|
|
31
|
+
.trim()
|
|
32
|
+
.min(1, "Route prefix is required")
|
|
33
|
+
.refine((value) => value.startsWith("/"), "Route prefix must start with '/'")
|
|
34
|
+
.refine((value) => !/\s/.test(value), "Route prefix cannot contain whitespace")
|
|
35
|
+
.refine((value) => value === "/" || !value.endsWith("/"), "Route prefix must not end with '/' unless it is '/'");
|
|
36
|
+
|
|
37
|
+
const bindingNameSchema = z
|
|
38
|
+
.string()
|
|
39
|
+
.trim()
|
|
40
|
+
.min(1, "Binding name is required")
|
|
41
|
+
.max(64, "Binding name must be at most 64 characters")
|
|
42
|
+
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, "Binding name must start with a letter and use letters, numbers, '_' or '-'");
|
|
43
|
+
|
|
44
|
+
const domainSchema = z
|
|
45
|
+
.string()
|
|
46
|
+
.trim()
|
|
47
|
+
.min(1, "Domain is required")
|
|
48
|
+
.refine((value) => {
|
|
49
|
+
const candidate = value.includes("://") ? value : `https://${value}`;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(candidate);
|
|
52
|
+
return parsed.hostname.length > 0;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}, "Domain must be a valid hostname or URL");
|
|
57
|
+
|
|
58
|
+
const envKeySchema = z.string().trim().min(1, "Env var value cannot be empty");
|
|
59
|
+
|
|
60
|
+
const createLytxAppConfigSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
enableRequestLogging: z.boolean().optional(),
|
|
63
|
+
db: dbConfigSchema.optional(),
|
|
64
|
+
dbAdapter: dbAdapterSchema.optional(),
|
|
65
|
+
useQueueIngestion: z.boolean().optional(),
|
|
66
|
+
includeLegacyTagRoutes: z.boolean().optional(),
|
|
67
|
+
trackingRoutePrefix: routePrefixSchema.optional(),
|
|
68
|
+
auth: z
|
|
69
|
+
.object({
|
|
70
|
+
emailPasswordEnabled: z.boolean().optional(),
|
|
71
|
+
requireEmailVerification: z.boolean().optional(),
|
|
72
|
+
socialProviders: z
|
|
73
|
+
.object({
|
|
74
|
+
google: z.boolean().optional(),
|
|
75
|
+
github: z.boolean().optional(),
|
|
76
|
+
})
|
|
77
|
+
.strict()
|
|
78
|
+
.optional(),
|
|
79
|
+
})
|
|
80
|
+
.strict()
|
|
81
|
+
.optional(),
|
|
82
|
+
tagRoutes: z
|
|
83
|
+
.object({
|
|
84
|
+
scriptPath: routePathSchema.optional(),
|
|
85
|
+
legacyScriptPath: routePathSchema.optional(),
|
|
86
|
+
eventPath: routePathSchema.optional(),
|
|
87
|
+
legacyEventPath: routePathSchema.optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict()
|
|
90
|
+
.optional(),
|
|
91
|
+
features: z
|
|
92
|
+
.object({
|
|
93
|
+
dashboard: z.boolean().optional(),
|
|
94
|
+
events: z.boolean().optional(),
|
|
95
|
+
auth: z.boolean().optional(),
|
|
96
|
+
ai: z.boolean().optional(),
|
|
97
|
+
tagScript: z.boolean().optional(),
|
|
98
|
+
reportBuilderEnabled: z.boolean().optional(),
|
|
99
|
+
askAiEnabled: z.boolean().optional(),
|
|
100
|
+
})
|
|
101
|
+
.strict()
|
|
102
|
+
.optional(),
|
|
103
|
+
names: z
|
|
104
|
+
.object({
|
|
105
|
+
d1Binding: bindingNameSchema.optional(),
|
|
106
|
+
eventsKvBinding: bindingNameSchema.optional(),
|
|
107
|
+
configKvBinding: bindingNameSchema.optional(),
|
|
108
|
+
sessionsKvBinding: bindingNameSchema.optional(),
|
|
109
|
+
queueBinding: bindingNameSchema.optional(),
|
|
110
|
+
durableObjectBinding: bindingNameSchema.optional(),
|
|
111
|
+
})
|
|
112
|
+
.strict()
|
|
113
|
+
.optional(),
|
|
114
|
+
domains: z
|
|
115
|
+
.object({
|
|
116
|
+
app: domainSchema.optional(),
|
|
117
|
+
tracking: domainSchema.optional(),
|
|
118
|
+
})
|
|
119
|
+
.strict()
|
|
120
|
+
.optional(),
|
|
121
|
+
env: z
|
|
122
|
+
.object({
|
|
123
|
+
BETTER_AUTH_SECRET: envKeySchema.optional(),
|
|
124
|
+
BETTER_AUTH_URL: z.string().trim().url("BETTER_AUTH_URL must be a valid URL").optional(),
|
|
125
|
+
ENCRYPTION_KEY: envKeySchema.optional(),
|
|
126
|
+
AI_API_KEY: envKeySchema.optional(),
|
|
127
|
+
AI_MODEL: envKeySchema.optional(),
|
|
128
|
+
LYTX_DOMAIN: domainSchema.optional(),
|
|
129
|
+
EMAIL_FROM: z.string().trim().email("EMAIL_FROM must be a valid email address").optional(),
|
|
130
|
+
})
|
|
131
|
+
.strict()
|
|
132
|
+
.optional(),
|
|
133
|
+
startupValidation: z
|
|
134
|
+
.object({
|
|
135
|
+
requireCoreEnv: z.boolean().optional(),
|
|
136
|
+
requireAiEnvWhenAskAiEnabled: z.boolean().optional(),
|
|
137
|
+
})
|
|
138
|
+
.strict()
|
|
139
|
+
.optional(),
|
|
140
|
+
})
|
|
141
|
+
.strict()
|
|
142
|
+
.superRefine((value, ctx) => {
|
|
143
|
+
if (value.db?.dbAdapter && value.dbAdapter && value.db.dbAdapter !== value.dbAdapter) {
|
|
144
|
+
ctx.addIssue({
|
|
145
|
+
code: z.ZodIssueCode.custom,
|
|
146
|
+
path: ["db", "dbAdapter"],
|
|
147
|
+
message: "db.dbAdapter must match top-level dbAdapter when both are provided",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (value.features?.dashboard === true && value.features?.auth === false) {
|
|
152
|
+
ctx.addIssue({
|
|
153
|
+
code: z.ZodIssueCode.custom,
|
|
154
|
+
path: ["features", "dashboard"],
|
|
155
|
+
message: "Dashboard cannot be enabled when auth is disabled",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (value.features?.reportBuilderEnabled === true && value.features?.dashboard === false) {
|
|
160
|
+
ctx.addIssue({
|
|
161
|
+
code: z.ZodIssueCode.custom,
|
|
162
|
+
path: ["features", "reportBuilderEnabled"],
|
|
163
|
+
message: "Report builder cannot be enabled when dashboard is disabled",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (value.features?.ai === false && value.features?.askAiEnabled === true) {
|
|
168
|
+
ctx.addIssue({
|
|
169
|
+
code: z.ZodIssueCode.custom,
|
|
170
|
+
path: ["features", "askAiEnabled"],
|
|
171
|
+
message: "Ask AI cannot be enabled when AI feature is disabled",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (value.features?.askAiEnabled === true && value.features?.reportBuilderEnabled === false) {
|
|
176
|
+
ctx.addIssue({
|
|
177
|
+
code: z.ZodIssueCode.custom,
|
|
178
|
+
path: ["features", "askAiEnabled"],
|
|
179
|
+
message: "Ask AI cannot be enabled when reportBuilderEnabled is false",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const emailPasswordEnabled = value.auth?.emailPasswordEnabled ?? true;
|
|
184
|
+
const googleExplicitlyDisabled = value.auth?.socialProviders?.google === false;
|
|
185
|
+
const githubExplicitlyDisabled = value.auth?.socialProviders?.github === false;
|
|
186
|
+
if (!emailPasswordEnabled && googleExplicitlyDisabled && githubExplicitlyDisabled) {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: z.ZodIssueCode.custom,
|
|
189
|
+
path: ["auth", "emailPasswordEnabled"],
|
|
190
|
+
message: "At least one auth method must be enabled",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const requireCoreEnv = value.startupValidation?.requireCoreEnv === true;
|
|
195
|
+
if (requireCoreEnv) {
|
|
196
|
+
const requiredCoreKeys: Array<keyof NonNullable<typeof value.env>> = [
|
|
197
|
+
"BETTER_AUTH_SECRET",
|
|
198
|
+
"BETTER_AUTH_URL",
|
|
199
|
+
"ENCRYPTION_KEY",
|
|
200
|
+
];
|
|
201
|
+
for (const key of requiredCoreKeys) {
|
|
202
|
+
if (!value.env?.[key]) {
|
|
203
|
+
ctx.addIssue({
|
|
204
|
+
code: z.ZodIssueCode.custom,
|
|
205
|
+
path: ["env", key],
|
|
206
|
+
message: `Missing required env var ${key} (required when startupValidation.requireCoreEnv is true)`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const requireAiEnv = value.startupValidation?.requireAiEnvWhenAskAiEnabled ?? true;
|
|
213
|
+
if (value.features?.askAiEnabled === true && requireAiEnv) {
|
|
214
|
+
if (!value.env?.AI_API_KEY) {
|
|
215
|
+
ctx.addIssue({
|
|
216
|
+
code: z.ZodIssueCode.custom,
|
|
217
|
+
path: ["env", "AI_API_KEY"],
|
|
218
|
+
message: "Missing required env var AI_API_KEY when Ask AI is enabled",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!value.env?.AI_MODEL) {
|
|
222
|
+
ctx.addIssue({
|
|
223
|
+
code: z.ZodIssueCode.custom,
|
|
224
|
+
path: ["env", "AI_MODEL"],
|
|
225
|
+
message: "Missing required env var AI_MODEL when Ask AI is enabled",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export type CreateLytxAppConfig = z.input<typeof createLytxAppConfigSchema>;
|
|
232
|
+
|
|
233
|
+
function formatValidationErrors(error: z.ZodError): string {
|
|
234
|
+
const lines = error.issues.map((issue) => {
|
|
235
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "config";
|
|
236
|
+
return `- ${path}: ${issue.message}`;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
"Invalid createLytxApp config:",
|
|
241
|
+
...lines,
|
|
242
|
+
`See ${CREATE_LYTX_APP_CONFIG_DOC_URL}`,
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function parseCreateLytxAppConfig(config: CreateLytxAppConfig): CreateLytxAppConfig {
|
|
247
|
+
const parsed = createLytxAppConfigSchema.safeParse(config ?? {});
|
|
248
|
+
if (!parsed.success) {
|
|
249
|
+
throw new Error(formatValidationErrors(parsed.error));
|
|
250
|
+
}
|
|
251
|
+
return parsed.data;
|
|
252
|
+
}
|