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,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
|
+
}
|