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,283 @@
1
+ "use client";
2
+ import { useMemo, useState } from "react";
3
+ import { resendVerificationEmail, signIn } from "@/app/providers/AuthProvider";
4
+ import { ThemeProvider } from "@/app/providers/ThemeProvider";
5
+
6
+ type AuthProviders = {
7
+ google: boolean;
8
+ github: boolean;
9
+ };
10
+
11
+ type LoginProps = {
12
+ authProviders?: AuthProviders;
13
+ emailPasswordEnabled?: boolean;
14
+ };
15
+
16
+ type AuthUiStatus =
17
+ | { type: "idle" }
18
+ | { type: "success"; message: string }
19
+ | { type: "error"; message: string }
20
+ | { type: "verify_email"; message: string };
21
+
22
+ function normalizeAuthErrorMessage(error: unknown) {
23
+ let message = "Sign in failed";
24
+
25
+ if (error instanceof Error) {
26
+ message = error.message;
27
+ } else if (typeof error === "string") {
28
+ message = error;
29
+ } else if (error && typeof error === "object") {
30
+ const maybeMessage = (error as { message?: unknown }).message;
31
+ const maybeError = (error as { error?: unknown }).error;
32
+ if (typeof maybeMessage === "string") message = maybeMessage;
33
+ else if (typeof maybeError === "string") message = maybeError;
34
+ }
35
+
36
+ // Map technical error messages to user-friendly ones
37
+ const lowerMessage = message.toLowerCase();
38
+ if (lowerMessage.includes("user not found") || lowerMessage.includes("no user found")) {
39
+ return "No account found with this email address.";
40
+ }
41
+ if (lowerMessage.includes("invalid password") || lowerMessage.includes("incorrect password")) {
42
+ return "Incorrect password. Please try again.";
43
+ }
44
+ if (lowerMessage.includes("invalid credentials")) {
45
+ return "Invalid email or password.";
46
+ }
47
+
48
+ return message;
49
+ }
50
+
51
+ function isEmailVerificationError(message: string) {
52
+ const normalized = message.toLowerCase();
53
+ return (
54
+ normalized.includes("verify") ||
55
+ normalized.includes("verification") ||
56
+ normalized.includes("email verification") ||
57
+ normalized.includes("unverified")
58
+ );
59
+ }
60
+
61
+ export function Login({ authProviders = { google: true, github: true }, emailPasswordEnabled = true }: LoginProps) {
62
+ const [status, setStatus] = useState<AuthUiStatus>({ type: "idle" });
63
+ const [email, setEmail] = useState("");
64
+ const [isResending, setIsResending] = useState(false);
65
+ const [pendingProvider, setPendingProvider] = useState<"google" | "github" | null>(null);
66
+
67
+ const canResend = useMemo(() => Boolean(email.trim()) && !isResending, [email, isResending]);
68
+ const resendLabel = isResending ? "Sending..." : "Resend verification email";
69
+
70
+ const handleResend = async () => {
71
+ if (!email.trim()) return;
72
+
73
+ setIsResending(true);
74
+ setStatus({ type: "idle" });
75
+
76
+ try {
77
+ await resendVerificationEmail(email);
78
+ setStatus({ type: "success", message: "Verification email sent. Please check your inbox." });
79
+ } catch (error) {
80
+ const errorMessage =
81
+ error instanceof Error ? error.message : "Unable to resend verification email";
82
+ setStatus({ type: "error", message: errorMessage });
83
+ } finally {
84
+ setIsResending(false);
85
+ }
86
+ };
87
+
88
+ return (
89
+ <ThemeProvider>
90
+ <div className="flex flex-col justify-center items-center min-h-screen py-12 font-sans bg-slate-50 text-slate-900 dark:bg-black dark:text-slate-100">
91
+ <div className="flex flex-col min-h-[200px] w-full justify-center items-center">
92
+ <a
93
+ href="/"
94
+ className="flex items-center gap-2 h-auto font-montserrat font-bold text-2xl tracking-tight"
95
+ >
96
+ <img src="/logo.png" alt="Lytx logo" className="h-7 w-7" />
97
+ <span>Lytx</span>
98
+ </a>
99
+ <div className="h-auto my-4">Sign in to your account</div>
100
+
101
+ {(authProviders.google || authProviders.github) ? (
102
+ <div className="flex flex-col gap-3 mb-6 px-4 w-full max-w-[300px]">
103
+ {authProviders.google ? (
104
+ <button
105
+ onClick={async () => {
106
+ setPendingProvider("google");
107
+ try {
108
+ await signIn("google");
109
+ } catch {
110
+ setPendingProvider(null);
111
+ }
112
+ }}
113
+ disabled={pendingProvider !== null}
114
+ type="button"
115
+ className={`flex cursor-pointer items-center justify-center gap-3 w-full py-3 px-4 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors active:scale-95 ${pendingProvider === "google" ? "opacity-70" : pendingProvider === "github" ? "opacity-50 pointer-events-none" : ""}`}
116
+ >
117
+ {pendingProvider === "google" ? (
118
+ <svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
119
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" />
120
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
121
+ </svg>
122
+ ) : (
123
+ <svg className="w-5 h-5" viewBox="0 0 24 24">
124
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
125
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
126
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
127
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
128
+ </svg>
129
+ )}
130
+ {pendingProvider === "google" ? "Connecting..." : "Continue with Google"}
131
+ </button>
132
+ ) : null}
133
+
134
+ {authProviders.github ? (
135
+ <button
136
+ onClick={async () => {
137
+ setPendingProvider("github");
138
+ try {
139
+ await signIn("github");
140
+ } catch {
141
+ setPendingProvider(null);
142
+ }
143
+ }}
144
+ disabled={pendingProvider !== null}
145
+ type="button"
146
+ className={`flex cursor-pointer items-center justify-center gap-3 w-full py-3 px-4 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors active:scale-95 ${pendingProvider === "github" ? "opacity-70" : pendingProvider === "google" ? "opacity-50 pointer-events-none" : ""}`}
147
+ >
148
+ {pendingProvider === "github" ? (
149
+ <svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
150
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" />
151
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
152
+ </svg>
153
+ ) : (
154
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
155
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
156
+ </svg>
157
+ )}
158
+ {pendingProvider === "github" ? "Connecting..." : "Continue with GitHub"}
159
+ </button>
160
+ ) : null}
161
+
162
+ {emailPasswordEnabled ? (
163
+ <div className="flex items-center my-4">
164
+ <div className="flex-1 border-t border-slate-200 dark:border-slate-700"></div>
165
+ <div className="px-4 text-slate-500 dark:text-slate-400 text-sm">or</div>
166
+ <div className="flex-1 border-t border-slate-200 dark:border-slate-700"></div>
167
+ </div>
168
+ ) : null}
169
+ </div>
170
+ ) : null}
171
+
172
+ {status.type !== "idle" ? (
173
+ <div className="px-4 w-full max-w-[300px] text-sm">
174
+ <div
175
+ className={
176
+ status.type === "success"
177
+ ? "text-green-600"
178
+ : status.type === "verify_email"
179
+ ? "text-amber-600"
180
+ : "text-red-600"
181
+ }
182
+ >
183
+ {status.message}
184
+ </div>
185
+ {status.type === "verify_email" ? (
186
+ <div className="mt-3 text-xs text-slate-600 dark:text-slate-400">
187
+ <div>Didn’t get the email? Check spam, then resend.</div>
188
+ </div>
189
+ ) : null}
190
+ </div>
191
+ ) : null}
192
+
193
+ {status.type === "verify_email" && (
194
+ <div className="px-4 w-full max-w-[300px] text-sm">
195
+ <button
196
+ type="button"
197
+ disabled={!canResend}
198
+ className="underline disabled:opacity-60"
199
+ onClick={handleResend}
200
+ >
201
+ {resendLabel}
202
+ </button>
203
+ </div>
204
+ )}
205
+
206
+ {emailPasswordEnabled ? (
207
+ <form
208
+ onSubmit={async (e) => {
209
+ e.preventDefault();
210
+ setStatus({ type: "idle" });
211
+
212
+ const formData = new FormData(e.currentTarget);
213
+ const emailValue = String(formData.get("email") || "");
214
+ const password = String(formData.get("password") || "");
215
+ setEmail(emailValue);
216
+
217
+ try {
218
+ await signIn("email", { email: emailValue, password });
219
+ } catch (error) {
220
+ const errorMessage = normalizeAuthErrorMessage(error);
221
+ if (isEmailVerificationError(errorMessage)) {
222
+ setStatus({
223
+ type: "verify_email",
224
+ message: "Email verification required. Please verify your email to sign in.",
225
+ });
226
+ } else {
227
+ setStatus({ type: "error", message: errorMessage });
228
+ }
229
+ // Keep the entered email available for resending.
230
+ }
231
+ }}
232
+ method="post" className="flex flex-col mt-2 px-4 gap-4" action="/auth/login">
233
+ <div className="text-left">
234
+ <label htmlFor="login-email" className="text-sm font-medium text-slate-700 dark:text-slate-300">Email address</label>
235
+ <input
236
+ id="login-email"
237
+ type="email"
238
+ placeholder="joe@cooldata.com"
239
+ required
240
+ name="email"
241
+ value={email}
242
+ onChange={(event) => setEmail(event.target.value)}
243
+ className="w-full border-b border-slate-200 dark:border-slate-700 bg-transparent mt-2 pr-4 py-2 text-base"
244
+ />
245
+ </div>
246
+ <div className="text-left">
247
+ <label htmlFor="login-password" className="text-sm font-medium text-slate-700 dark:text-slate-300">Password</label>
248
+ <input
249
+ id="login-password"
250
+ type="password"
251
+ placeholder="**********"
252
+ required
253
+ name="password"
254
+ className="w-full mt-2 border-b border-slate-200 dark:border-slate-700 bg-transparent pr-4 py-2 text-base"
255
+ />
256
+ <input
257
+ type="hidden"
258
+ name="path"
259
+ defaultValue="/login"
260
+ />
261
+ </div>
262
+ <div>
263
+ <button
264
+ type="submit"
265
+ className="w-full mt-2 bg-slate-900 text-white dark:bg-white dark:text-black py-2 border-none rounded-lg text-base cursor-pointer hover:bg-slate-800 dark:hover:bg-slate-200 transition-colors"
266
+ >
267
+ Sign In
268
+ </button>
269
+ </div>
270
+ </form>
271
+ ) : (
272
+ <div className="text-sm text-slate-600 dark:text-slate-400 px-4 w-full max-w-[300px]">
273
+ Email/password sign-in is disabled for this deployment.
274
+ </div>
275
+ )}
276
+ <div className="h-5 my-4">
277
+ <a href="/signup" className="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white transition-colors">Create an account</a>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </ThemeProvider>
282
+ )
283
+ }
@@ -0,0 +1,120 @@
1
+ import { MarketingLayout } from "@/app/components/marketing/MarketingLayout";
2
+ import { SectionHeading } from "@/app/components/marketing/SectionHeading";
3
+
4
+ export function PrivacyPolicy() {
5
+ return (
6
+ <MarketingLayout>
7
+ <section className="pt-32 pb-20">
8
+ <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
9
+ <SectionHeading
10
+ as="h1"
11
+ badge="Legal"
12
+ title="Privacy Policy"
13
+ subtitle="Lytx is a privacy-first analytics platform. This policy explains how Riche Ventures, Inc. collects and uses information when you use the Lytx product."
14
+ />
15
+
16
+ <div className="space-y-8 text-slate-600 dark:text-slate-400">
17
+ <p className="text-sm text-slate-500 dark:text-slate-500">Last updated: January 29, 2026</p>
18
+
19
+ <div className="space-y-3">
20
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Who we are</h2>
21
+ <p>
22
+ Lytx is a product of Riche Ventures, Inc. ("Riche Ventures", "we", "us").
23
+ This Privacy Policy applies to the Lytx website, application, and analytics services
24
+ (the "Services").
25
+ </p>
26
+ </div>
27
+
28
+ <div className="space-y-3">
29
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Information we collect</h2>
30
+ <ul className="space-y-2 list-disc pl-5">
31
+ <li>
32
+ Account information such as name, email address, and login credentials.
33
+ </li>
34
+ <li>
35
+ Usage data about how you use the Services, including feature usage, settings, and
36
+ performance metrics.
37
+ </li>
38
+ <li>
39
+ Support and communications you send to us.
40
+ </li>
41
+ </ul>
42
+ </div>
43
+
44
+ <div className="space-y-3">
45
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Analytics data from your sites</h2>
46
+ <p>
47
+ When you install the Lytx script on your site, Lytx processes analytics data on your
48
+ behalf. You are the controller of that data and we act as a processor. We do not use
49
+ cookies or fingerprinting in the Lytx analytics script. We do not store full IP
50
+ addresses in analytics data; any transient processing of IP addresses is limited to
51
+ security and abuse prevention.
52
+ </p>
53
+ </div>
54
+
55
+ <div className="space-y-3">
56
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">How we use information</h2>
57
+ <ul className="space-y-2 list-disc pl-5">
58
+ <li>Provide, operate, and maintain the Services.</li>
59
+ <li>Authenticate users, secure accounts, and prevent abuse.</li>
60
+ <li>Improve product performance and user experience.</li>
61
+ <li>Respond to support requests and send service communications.</li>
62
+ </ul>
63
+ </div>
64
+
65
+ <div className="space-y-3">
66
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Sharing of information</h2>
67
+ <p>
68
+ We share information only with service providers needed to deliver the Services (such
69
+ as hosting and email), to comply with legal obligations, or in connection
70
+ with a business transfer. We do not sell your data.
71
+ </p>
72
+ </div>
73
+
74
+ <div className="space-y-3">
75
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Data retention</h2>
76
+ <p>
77
+ We retain account and usage data for as long as your account is active or as needed to
78
+ provide the Services. You can request deletion of your account and associated data.
79
+ Analytics data retention for customer sites is controlled by the customer.
80
+ </p>
81
+ </div>
82
+
83
+ <div className="space-y-3">
84
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Your choices</h2>
85
+ <ul className="space-y-2 list-disc pl-5">
86
+ <li>Access and update your account information in your settings.</li>
87
+ <li>Request account deletion by contacting us.</li>
88
+ <li>Opt out of non-essential communications.</li>
89
+ </ul>
90
+ </div>
91
+
92
+ <div className="space-y-3">
93
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Contact</h2>
94
+ <p>
95
+ Riche Ventures, Inc.<br />
96
+ 390 NE 191st St STE 27725<br />
97
+ Miami, FL 33179<br />
98
+ Email:{" "}
99
+ <a
100
+ href="mailto:legal@yourdomain.com"
101
+ className="text-slate-600 hover:text-amber-600 dark:text-slate-400 dark:hover:text-amber-400 underline underline-offset-4"
102
+ >
103
+ legal@yourdomain.com
104
+ </a>
105
+ </p>
106
+ </div>
107
+
108
+ <div className="space-y-3">
109
+ <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Changes to this policy</h2>
110
+ <p>
111
+ We may update this Privacy Policy from time to time. If we make material changes, we
112
+ will update the date above and post the revised policy.
113
+ </p>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </section>
118
+ </MarketingLayout>
119
+ );
120
+ }
@@ -0,0 +1,267 @@
1
+ "use client";
2
+ import { useMemo, useState } from "react";
3
+ import { emailSignUp, resendVerificationEmail, signIn } from "@/app/providers/AuthProvider";
4
+ import { ThemeProvider } from "@/app/providers/ThemeProvider";
5
+
6
+ type AuthProviders = {
7
+ google: boolean;
8
+ github: boolean;
9
+ };
10
+
11
+ type SignupProps = {
12
+ authProviders?: AuthProviders;
13
+ emailPasswordEnabled?: boolean;
14
+ };
15
+
16
+ type SignupStatus =
17
+ | { type: "idle" }
18
+ | { type: "submitting" }
19
+ | { type: "success"; message: string }
20
+ | { type: "error"; message: string };
21
+
22
+ export function Signup({ authProviders = { google: true, github: true }, emailPasswordEnabled = true }: SignupProps) {
23
+ const [status, setStatus] = useState<SignupStatus>({ type: "idle" });
24
+ const [email, setEmail] = useState("");
25
+ const [password, setPassword] = useState("");
26
+ const [verifyPassword, setVerifyPassword] = useState("");
27
+ const [isResending, setIsResending] = useState(false);
28
+ const [pendingProvider, setPendingProvider] = useState<"google" | "github" | null>(null);
29
+
30
+ const canSubmit = useMemo(() => {
31
+ if (!emailPasswordEnabled) return false;
32
+ if (status.type === "submitting") return false;
33
+ return Boolean(email.trim()) && Boolean(password) && Boolean(verifyPassword);
34
+ }, [emailPasswordEnabled, email, password, verifyPassword, status.type]);
35
+
36
+ const canResend = useMemo(() => Boolean(email.trim()) && !isResending, [email, isResending]);
37
+
38
+ const handleResend = async () => {
39
+ if (!email.trim()) return;
40
+
41
+ setIsResending(true);
42
+ try {
43
+ await resendVerificationEmail(email);
44
+ setStatus({
45
+ type: "success",
46
+ message: "Verification email sent. Please check your inbox to finish signing up.",
47
+ });
48
+ } catch (error) {
49
+ const errorMessage = error instanceof Error ? error.message : "Unable to resend verification email";
50
+ setStatus({ type: "error", message: errorMessage });
51
+ } finally {
52
+ setIsResending(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <ThemeProvider>
58
+ <div className="flex flex-col justify-center items-center min-h-screen py-12 font-sans bg-slate-50 text-slate-900 dark:bg-black dark:text-slate-100">
59
+ <div className="flex flex-col min-h-[200px] w-full justify-center items-center">
60
+ <div className="flex items-center gap-2 h-auto font-montserrat font-bold text-2xl tracking-tight">
61
+ <img src="/logo.png" alt="Lytx logo" className="h-7 w-7" />
62
+ <span>Lytx</span>
63
+ </div>
64
+ <div className="h-auto my-4">Register your account</div>
65
+
66
+ {(authProviders.google || authProviders.github) ? (
67
+ <div className="flex flex-col gap-3 mb-6 px-4 w-full max-w-[300px]">
68
+ {authProviders.google ? (
69
+ <button
70
+ onClick={async () => {
71
+ setPendingProvider("google");
72
+ try {
73
+ await signIn("google");
74
+ } catch {
75
+ setPendingProvider(null);
76
+ }
77
+ }}
78
+ disabled={pendingProvider !== null}
79
+ type="button"
80
+ className={`flex items-center justify-center gap-3 w-full py-3 px-4 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors active:scale-95 ${pendingProvider === "google" ? "opacity-70" : pendingProvider === "github" ? "opacity-50 pointer-events-none" : ""}`}
81
+ >
82
+ {pendingProvider === "google" ? (
83
+ <svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
84
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" />
85
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
86
+ </svg>
87
+ ) : (
88
+ <svg className="w-5 h-5" viewBox="0 0 24 24">
89
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
90
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
91
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
92
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
93
+ </svg>
94
+ )}
95
+ {pendingProvider === "google" ? "Connecting..." : "Continue with Google"}
96
+ </button>
97
+ ) : null}
98
+
99
+ {authProviders.github ? (
100
+ <button
101
+ onClick={async () => {
102
+ setPendingProvider("github");
103
+ try {
104
+ await signIn("github");
105
+ } catch {
106
+ setPendingProvider(null);
107
+ }
108
+ }}
109
+ disabled={pendingProvider !== null}
110
+ type="button"
111
+ className={`flex items-center justify-center gap-3 w-full py-3 px-4 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors active:scale-95 ${pendingProvider === "github" ? "opacity-70" : pendingProvider === "google" ? "opacity-50 pointer-events-none" : ""}`}
112
+ >
113
+ {pendingProvider === "github" ? (
114
+ <svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
115
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" />
116
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
117
+ </svg>
118
+ ) : (
119
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
120
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
121
+ </svg>
122
+ )}
123
+ {pendingProvider === "github" ? "Connecting..." : "Continue with GitHub"}
124
+ </button>
125
+ ) : null}
126
+
127
+ {emailPasswordEnabled ? (
128
+ <div className="flex items-center my-4">
129
+ <div className="flex-1 border-t border-slate-200 dark:border-slate-700"></div>
130
+ <div className="px-4 text-slate-500 dark:text-slate-400 text-sm">or</div>
131
+ <div className="flex-1 border-t border-slate-200 dark:border-slate-700"></div>
132
+ </div>
133
+ ) : null}
134
+ </div>
135
+ ) : null}
136
+
137
+ {status.type !== "idle" ? (
138
+ <div className="px-4 w-full max-w-[300px] text-sm">
139
+ <div
140
+ className={
141
+ status.type === "success"
142
+ ? "text-green-600"
143
+ : status.type === "submitting"
144
+ ? "text-slate-600 dark:text-slate-400"
145
+ : "text-red-600"
146
+ }
147
+ >
148
+ {status.type === "submitting" ? "Creating your account..." : status.message}
149
+ </div>
150
+ </div>
151
+ ) : null}
152
+
153
+ {emailPasswordEnabled ? (
154
+ <form
155
+ className="flex flex-col mt-2 px-4 gap-4"
156
+ onSubmit={async (event) => {
157
+ event.preventDefault();
158
+
159
+ setStatus({ type: "idle" });
160
+
161
+ const trimmedEmail = email.trim();
162
+ if (!trimmedEmail) {
163
+ setStatus({ type: "error", message: "Email is required." });
164
+ return;
165
+ }
166
+
167
+ if (password !== verifyPassword) {
168
+ setStatus({ type: "error", message: "Passwords do not match." });
169
+ return;
170
+ }
171
+
172
+ setStatus({ type: "submitting" });
173
+
174
+ try {
175
+ await emailSignUp(trimmedEmail, password, trimmedEmail);
176
+ setStatus({
177
+ type: "success",
178
+ message:
179
+ "Almost there — we sent a verification email. Verify your email, then come back to sign in.",
180
+ });
181
+ } catch (error) {
182
+ const errorMessage = error instanceof Error ? error.message : "Sign up failed";
183
+ setStatus({ type: "error", message: errorMessage });
184
+ }
185
+ }}
186
+ >
187
+ <div className="text-left">
188
+ <label htmlFor="signup-email" className="text-sm font-medium text-slate-700 dark:text-slate-300">Email address</label>
189
+ <input
190
+ id="signup-email"
191
+ type="email"
192
+ placeholder="joe@cooldata.com"
193
+ required
194
+ name="email"
195
+ value={email}
196
+ onChange={(event) => setEmail(event.target.value)}
197
+ className="w-full border-b border-slate-200 dark:border-slate-700 bg-transparent mt-2 pr-4 py-2 text-base"
198
+ />
199
+ </div>
200
+ <div className="text-left">
201
+ <label htmlFor="signup-password" className="text-sm font-medium text-slate-700 dark:text-slate-300">Password</label>
202
+ <input
203
+ id="signup-password"
204
+ type="password"
205
+ placeholder="**********"
206
+ required
207
+ name="password"
208
+ value={password}
209
+ onChange={(event) => setPassword(event.target.value)}
210
+ className="w-full mt-2 border-b border-slate-200 dark:border-slate-700 bg-transparent pr-4 py-2 text-base"
211
+ />
212
+ </div>
213
+ <div className="text-left">
214
+ <label htmlFor="signup-verify-password" className="text-sm font-medium text-slate-700 dark:text-slate-300">Verify Password</label>
215
+ <input
216
+ id="signup-verify-password"
217
+ type="password"
218
+ placeholder="**********"
219
+ required
220
+ name="verifypassword"
221
+ value={verifyPassword}
222
+ onChange={(event) => setVerifyPassword(event.target.value)}
223
+ className="w-full mt-2 border-b border-slate-200 dark:border-slate-700 bg-transparent pr-4 py-2 text-base"
224
+ />
225
+ <input
226
+ type="hidden"
227
+ name="path"
228
+ defaultValue="/signup"
229
+ />
230
+ </div>
231
+ <div>
232
+ <button
233
+ type="submit"
234
+ disabled={!canSubmit}
235
+ className="w-full mt-2 bg-slate-900 text-white dark:bg-white dark:text-black py-2 border-none rounded-lg text-base cursor-pointer hover:bg-slate-800 dark:hover:bg-slate-200 transition-colors disabled:opacity-60"
236
+ >
237
+ {status.type === "submitting" ? "Creating..." : "Sign Up"}
238
+ </button>
239
+ </div>
240
+ </form>
241
+ ) : (
242
+ <div className="text-sm text-slate-600 dark:text-slate-400 px-4 w-full max-w-[300px]">
243
+ Email/password signup is disabled for this deployment.
244
+ </div>
245
+ )}
246
+
247
+ {status.type === "success" ? (
248
+ <div className="mt-4 px-4 w-full max-w-[300px] text-sm">
249
+ <button
250
+ type="button"
251
+ disabled={!canResend}
252
+ className="underline disabled:opacity-60"
253
+ onClick={handleResend}
254
+ >
255
+ {isResending ? "Sending..." : "Resend verification email"}
256
+ </button>
257
+ </div>
258
+ ) : null}
259
+
260
+ <div className="h-5 my-4">
261
+ <a href="/login" className="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white transition-colors">Sign in to your account</a>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </ThemeProvider>
266
+ )
267
+ }