diff --git a/packages/website/src/cloud-signup.ts b/packages/website/src/cloud-signup.ts deleted file mode 100644 index d5cee1cfb..000000000 --- a/packages/website/src/cloud-signup.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { env } from "cloudflare:workers"; -import { createServerFn } from "@tanstack/react-start"; - -export interface CloudSignupInput { - email: string; - name?: string; - company?: string; - role?: string; - message: string; - honeypot?: string; -} - -const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - -function asString(v: unknown, max: number): string { - if (typeof v !== "string") throw new Error("expected string"); - const trimmed = v.trim(); - if (trimmed.length > max) throw new Error("field too long"); - return trimmed; -} - -function validate(raw: unknown): CloudSignupInput { - if (typeof raw !== "object" || raw === null) throw new Error("invalid input"); - const r = raw as Record; - - const email = asString(r.email, 320); - if (!EMAIL_RE.test(email)) throw new Error("invalid email"); - - const message = asString(r.message, 4000); - if (message.length === 0) throw new Error("message required"); - - const name = r.name === undefined || r.name === "" ? undefined : asString(r.name, 200); - const company = - r.company === undefined || r.company === "" ? undefined : asString(r.company, 200); - const role = r.role === undefined || r.role === "" ? undefined : asString(r.role, 200); - const honeypot = typeof r.honeypot === "string" ? r.honeypot : ""; - - return { email, name, company, role, message, honeypot }; -} - -function truncate(s: string, max: number): string { - return s.length <= max ? s : `${s.slice(0, max - 1)}…`; -} - -function buildEmbed(input: CloudSignupInput) { - const fields: Array<{ name: string; value: string; inline?: boolean }> = [ - { name: "Email", value: truncate(input.email, 1024), inline: true }, - ]; - if (input.name) { - fields.push({ name: "Name", value: truncate(input.name, 1024), inline: true }); - } - if (input.company) { - fields.push({ name: "Company", value: truncate(input.company, 1024), inline: true }); - } - if (input.role) { - fields.push({ name: "Role", value: truncate(input.role, 1024), inline: true }); - } - fields.push({ name: "Message", value: truncate(input.message, 1024) }); - - return { - title: "Paseo Cloud signup", - color: 0x5865f2, - fields, - timestamp: new Date().toISOString(), - }; -} - -export const submitCloudSignup = createServerFn({ method: "POST" }) - .inputValidator(validate) - .handler(async ({ data }) => { - if (data.honeypot) return { ok: true }; - - const url = (env as { DISCORD_WEBHOOK_URL?: string }).DISCORD_WEBHOOK_URL; - if (!url) throw new Error("webhook not configured"); - - const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ embeds: [buildEmbed(data)] }), - }); - - if (!res.ok) throw new Error(`webhook ${res.status}`); - return { ok: true }; - }); diff --git a/packages/website/src/components/agent-icons.tsx b/packages/website/src/components/agent-icons.tsx new file mode 100644 index 000000000..7b6b66146 --- /dev/null +++ b/packages/website/src/components/agent-icons.tsx @@ -0,0 +1,84 @@ +// Agent provider brand marks, shared by the landing page and the Hub page. + +import type * as React from "react"; + +export function ClaudeCodeIcon(props: React.SVGProps) { + return ( + + ); +} + +export function CodexIcon(props: React.SVGProps) { + return ( + + ); +} + +export function OpenCodeIcon(props: React.SVGProps) { + return ( + + ); +} + +export function CursorIcon(props: React.SVGProps) { + return ( + + ); +} + +export function PiIcon(props: React.SVGProps) { + return ( + + ); +} diff --git a/packages/website/src/components/brand-icons.tsx b/packages/website/src/components/brand-icons.tsx new file mode 100644 index 000000000..0b48a9d5f --- /dev/null +++ b/packages/website/src/components/brand-icons.tsx @@ -0,0 +1,46 @@ +// Third-party brand marks. Paths come from Simple Icons; Slack is the 13.x path, +// kept here because Slack was pulled from later releases. + +import type * as React from "react"; + +export function GitHubIcon(props: React.SVGProps) { + return ( + + ); +} + +export function DiscordIcon(props: React.SVGProps) { + return ( + + ); +} + +export function SlackIcon(props: React.SVGProps) { + return ( + + ); +} diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 5c0ca9fa0..a3195f177 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -43,8 +43,15 @@ import { GlobeIcon, } from "~/downloads"; import { useRelease } from "~/routes/__root"; -import { Mic } from "lucide-react"; import { HeroMockup } from "~/components/hero-mockup"; +import { + ClaudeCodeIcon, + CodexIcon, + CursorIcon, + OpenCodeIcon, + PiIcon, +} from "~/components/agent-icons"; +import { DiscordIcon, GitHubIcon, SlackIcon } from "~/components/brand-icons"; import { ClaudeIcon } from "~/components/mockup"; import { FAQItem } from "~/components/faq-item"; import { SiteFooter } from "~/components/site-footer"; @@ -90,11 +97,8 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { + - - - - @@ -246,10 +250,12 @@ function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) { function FeatureSection({ title, description, + badge, children, }: { title: string; description: string; + badge?: string; children: React.ReactNode; }) { return ( @@ -259,16 +265,31 @@ function FeatureSection({ viewport={VIEWPORT_60} transition={EASE_OUT_05} > - + {children} ); } -function SectionTitle({ title, description }: { title: string; description: string }) { +function SectionTitle({ + title, + description, + badge, +}: { + title: string; + description: string; + badge?: string; +}) { return (
-

{title}

+
+

{title}

+ {badge && ( + + {badge} + + )} +

{description}

); @@ -837,371 +858,6 @@ function ShipPanel() { ); } -function SplitPanelsSection() { - return ( - -
-
- -
- -
- - -
-
-
-
-
- ); -} - -function PanelTile({ label, className }: { label: string; className: string }) { - return ( -
- {label} -
- ); -} - -function ServiceProxySection() { - const workspaces = [ - { name: "fix-auth", url: "web.fix-auth.my-app.localhost" }, - { name: "add-search", url: "web.add-search.my-app.localhost" }, - { name: "upgrade-deps", url: "web.upgrade-deps.my-app.localhost" }, - ]; - - return ( - -
-
- {/* Project */} -
- - - - my-app -
- - {/* Workspaces indented */} -
- {workspaces.map((ws) => ( -
-
-
- {ws.name} - npm run dev -
- {ws.url} -
- ))} -
-
-
- - ); -} - -function ShortcutsSection() { - const shortcuts = [ - { keys: ["⌘", "1-9"], action: "Switch panels" }, - { keys: ["⌘", "D"], action: "Split vertical" }, - { keys: ["⌘", "Shift", "D"], action: "Split horizontal" }, - { keys: ["⌘", "W"], action: "Close panel" }, - { keys: ["⌘", "N"], action: "New agent" }, - { keys: ["⌘", "K"], action: "Command palette" }, - ]; - - return ( - -
- {shortcuts.map((s) => ( -
- {s.action} -
- {s.keys.map((k) => ( - - {k} - - ))} -
-
- ))} -
-
- ); -} - -interface VoiceBarProps { - index: number; - barCount: number; -} - -function VoiceBar({ index, barCount }: VoiceBarProps) { - const style = React.useMemo(() => { - const center = barCount / 2; - const dist = Math.abs(index - center) / center; - const envelope = 1 - dist * dist; - const minH = 4; - const maxH = 56; - const baseH = minH + (maxH - minH) * envelope; - const jitter = Math.sin(index * 2.3) * 0.3 + Math.cos(index * 1.7) * 0.2; - const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(index * 0.8)))); - return { - height: h, - animationName: "voice-bar", - animationDuration: `${800 + (index % 5) * 200}ms`, - animationTimingFunction: "ease-in-out", - animationIterationCount: "infinite", - animationDirection: "alternate" as const, - animationDelay: `${(index % 7) * 80}ms`, - }; - }, [index, barCount]); - return
; -} - -const VOICE_BAR_COUNT = 48; -const VOICE_BAR_INDICES = Array.from({ length: VOICE_BAR_COUNT }, (_, i) => i); - -function VoiceWaveform() { - return ( -
- {VOICE_BAR_INDICES.map((i) => ( - - ))} -
- ); -} - -const USER_WORDS = - "Refactor the auth middleware to use the new session store, then run the test suite".split(" "); -const RESPONSE_WORDS = - "I'll update the auth middleware to use SessionStore instead of the legacy cookie-based approach. Let me refactor the middleware and update the tests.".split( - " ", - ); -const DICTATION_LAG = 2; -const RESPONSE_LAG = 3; -const WORD_APPEAR_MS = 150; -const RESPONSE_WORD_MS = 60; -const PHASE_GAP_MS = 800; -const LOOP_PAUSE_MS = 3000; - -type VoicePhase = - | "dictation" - | "dictation-flush" - | "pause" - | "response" - | "response-flush" - | "done"; - -function useVoiceConversation() { - const [phase, setPhase] = React.useState("dictation"); - const [wordIndex, setWordIndex] = React.useState(0); - - React.useEffect(() => { - if (phase === "dictation") { - if (wordIndex < USER_WORDS.length) { - const t = setTimeout(() => setWordIndex((w) => w + 1), WORD_APPEAR_MS); - return () => clearTimeout(t); - } - setPhase("dictation-flush"); - setWordIndex(0); - return; - } - if (phase === "dictation-flush") { - if (wordIndex < DICTATION_LAG) { - const t = setTimeout(() => setWordIndex((w) => w + 1), WORD_APPEAR_MS); - return () => clearTimeout(t); - } - const t = setTimeout(() => { - setPhase("pause"); - }, PHASE_GAP_MS); - return () => clearTimeout(t); - } - if (phase === "pause") { - const t = setTimeout(() => { - setPhase("response"); - setWordIndex(0); - }, PHASE_GAP_MS); - return () => clearTimeout(t); - } - if (phase === "response") { - if (wordIndex < RESPONSE_WORDS.length) { - const t = setTimeout(() => setWordIndex((w) => w + 1), RESPONSE_WORD_MS); - return () => clearTimeout(t); - } - setPhase("response-flush"); - setWordIndex(0); - return; - } - if (phase === "response-flush") { - if (wordIndex < RESPONSE_LAG) { - const t = setTimeout(() => setWordIndex((w) => w + 1), RESPONSE_WORD_MS); - return () => clearTimeout(t); - } - const t = setTimeout(() => { - setPhase("done"); - }, LOOP_PAUSE_MS); - return () => clearTimeout(t); - } - if (phase === "done") { - const t = setTimeout(() => { - setPhase("dictation"); - setWordIndex(0); - }, 0); - return () => clearTimeout(t); - } - }, [phase, wordIndex]); - - // Compute effective word indices for rendering - let dictationWordIndex: number; - if (phase === "dictation") { - dictationWordIndex = wordIndex; - } else if (phase === "dictation-flush") { - dictationWordIndex = USER_WORDS.length + wordIndex; - } else { - dictationWordIndex = USER_WORDS.length + DICTATION_LAG; - } - - let responseWordIndex: number; - if (phase === "response") { - responseWordIndex = wordIndex; - } else if (phase === "response-flush") { - responseWordIndex = RESPONSE_WORDS.length + wordIndex; - } else if (phase === "done") { - responseWordIndex = RESPONSE_WORDS.length + RESPONSE_LAG; - } else { - responseWordIndex = 0; - } - - const showResponse = phase === "response" || phase === "response-flush" || phase === "done"; - - return { dictationWordIndex, responseWordIndex, showResponse }; -} - -function makeWordKey(words: string[], i: number): string { - const word = words[i]; - let occurrence = 0; - for (let j = 0; j < i; j++) { - if (words[j] === word) occurrence++; - } - return `${word}#${occurrence}`; -} - -function WordSpan({ word, confirmed }: { word: string; confirmed: boolean }) { - return ( - - {word}{" "} - - ); -} - -function StreamingWords({ - words, - wordIndex, - confirmLag = 2, -}: { - words: string[]; - wordIndex: number; - confirmLag?: number; -}) { - return ( -
- {/* Invisible full text to reserve height at any viewport width */} -

- {words.join(" ")} -

- {/* Visible streaming text overlaid */} -

- {words.map((word, i) => { - if (i >= wordIndex) return null; - const confirmed = i < wordIndex - confirmLag; - return ; - })} -

-
- ); -} - -function LocalVoiceSection() { - const { dictationWordIndex, responseWordIndex, showResponse } = useVoiceConversation(); - - return ( - -
-
- {/* Waveform area */} -
- -
- - {/* User dictation */} -
-
- -
-
- -
-
- - {/* Agent response — always rendered to reserve space */} -
-
- -
-
- -
-
-
-
-
- ); -} - function GetStarted() { return (
@@ -1308,87 +964,6 @@ function ServerInstallButton() { ); } -function ClaudeCodeIcon(props: React.SVGProps) { - return ( - - ); -} - -function CodexIcon(props: React.SVGProps) { - return ( - - ); -} - -function OpenCodeIcon(props: React.SVGProps) { - return ( - - ); -} - -function CursorIcon(props: React.SVGProps) { - return ( - - ); -} - -function PiIcon(props: React.SVGProps) { - return ( - - ); -} - const bashKeywords = new Set([ "while", "do", @@ -1894,6 +1469,40 @@ function FAQ() { ); } +const HUB_SURFACES = [ + { name: "GitHub", icon: }, + { name: "Slack", icon: }, + { name: "Discord", icon: }, +]; + +function HubSection() { + return ( + +
+ {HUB_SURFACES.map((surface) => ( +
+ {surface.icon} + {surface.name} +
+ ))} +
+ + Learn more + +
+ ); +} + function SponsorCTA() { return ( Changelog + + Hub + - + - - - + {stars && {stars}}
diff --git a/packages/website/src/llms.ts b/packages/website/src/llms.ts index 13efc58fa..a10e90c72 100644 --- a/packages/website/src/llms.ts +++ b/packages/website/src/llms.ts @@ -60,7 +60,7 @@ ${agents} - [Changelog](${SITE_URL}/changelog): Release notes for the Paseo daemon, CLI, desktop, and mobile apps. - [Download](${SITE_URL}/download): Install Paseo on Mac, Windows, Linux, iOS, Android, or run the web app. -- [Paseo Cloud](${SITE_URL}/cloud): Waitlist for the hosted multi-user version of Paseo. +- [Paseo Hub](${SITE_URL}/hub): Optional service above your daemons that adds GitHub, Slack, and Discord triggers. Self-hosted, private beta. - [Blog](${SITE_URL}/blog): Updates and technical posts from the Paseo team. - [Privacy](${SITE_URL}/privacy): Privacy policy. - [GitHub](https://github.com/getpaseo/paseo): Source code, issues, and releases. diff --git a/packages/website/src/routeTree.gen.ts b/packages/website/src/routeTree.gen.ts index 5fbe9db61..dfae74086 100644 --- a/packages/website/src/routeTree.gen.ts +++ b/packages/website/src/routeTree.gen.ts @@ -26,6 +26,7 @@ import { Route as MinionCodeRouteImport } from "./routes/minion-code"; import { Route as KimiRouteImport } from "./routes/kimi"; import { Route as KiloRouteImport } from "./routes/kilo"; import { Route as JunieRouteImport } from "./routes/junie"; +import { Route as HubRouteImport } from "./routes/hub"; import { Route as HermesRouteImport } from "./routes/hermes"; import { Route as GrokRouteImport } from "./routes/grok"; import { Route as GooseRouteImport } from "./routes/goose"; @@ -46,7 +47,6 @@ import { Route as CortexCodeRouteImport } from "./routes/cortex-code"; import { Route as CopilotRouteImport } from "./routes/copilot"; import { Route as CodexRouteImport } from "./routes/codex"; import { Route as CodebuddyRouteImport } from "./routes/codebuddy"; -import { Route as CloudRouteImport } from "./routes/cloud"; import { Route as ClineRouteImport } from "./routes/cline"; import { Route as ClaudeCodeRouteImport } from "./routes/claude-code"; import { Route as ChangelogRouteImport } from "./routes/changelog"; @@ -154,6 +154,11 @@ const JunieRoute = JunieRouteImport.update({ path: "/junie", getParentRoute: () => rootRouteImport, } as any); +const HubRoute = HubRouteImport.update({ + id: "/hub", + path: "/hub", + getParentRoute: () => rootRouteImport, +} as any); const HermesRoute = HermesRouteImport.update({ id: "/hermes", path: "/hermes", @@ -254,11 +259,6 @@ const CodebuddyRoute = CodebuddyRouteImport.update({ path: "/codebuddy", getParentRoute: () => rootRouteImport, } as any); -const CloudRoute = CloudRouteImport.update({ - id: "/cloud", - path: "/cloud", - getParentRoute: () => rootRouteImport, -} as any); const ClineRoute = ClineRouteImport.update({ id: "/cline", path: "/cline", @@ -378,7 +378,6 @@ export interface FileRoutesByFullPath { "/changelog": typeof ChangelogRoute; "/claude-code": typeof ClaudeCodeRoute; "/cline": typeof ClineRoute; - "/cloud": typeof CloudRoute; "/codebuddy": typeof CodebuddyRoute; "/codex": typeof CodexRoute; "/copilot": typeof CopilotRoute; @@ -399,6 +398,7 @@ export interface FileRoutesByFullPath { "/goose": typeof GooseRoute; "/grok": typeof GrokRoute; "/hermes": typeof HermesRoute; + "/hub": typeof HubRoute; "/junie": typeof JunieRoute; "/kilo": typeof KiloRoute; "/kimi": typeof KimiRoute; @@ -438,7 +438,6 @@ export interface FileRoutesByTo { "/changelog": typeof ChangelogRoute; "/claude-code": typeof ClaudeCodeRoute; "/cline": typeof ClineRoute; - "/cloud": typeof CloudRoute; "/codebuddy": typeof CodebuddyRoute; "/codex": typeof CodexRoute; "/copilot": typeof CopilotRoute; @@ -458,6 +457,7 @@ export interface FileRoutesByTo { "/goose": typeof GooseRoute; "/grok": typeof GrokRoute; "/hermes": typeof HermesRoute; + "/hub": typeof HubRoute; "/junie": typeof JunieRoute; "/kilo": typeof KiloRoute; "/kimi": typeof KimiRoute; @@ -499,7 +499,6 @@ export interface FileRoutesById { "/changelog": typeof ChangelogRoute; "/claude-code": typeof ClaudeCodeRoute; "/cline": typeof ClineRoute; - "/cloud": typeof CloudRoute; "/codebuddy": typeof CodebuddyRoute; "/codex": typeof CodexRoute; "/copilot": typeof CopilotRoute; @@ -520,6 +519,7 @@ export interface FileRoutesById { "/goose": typeof GooseRoute; "/grok": typeof GrokRoute; "/hermes": typeof HermesRoute; + "/hub": typeof HubRoute; "/junie": typeof JunieRoute; "/kilo": typeof KiloRoute; "/kimi": typeof KimiRoute; @@ -562,7 +562,6 @@ export interface FileRouteTypes { | "/changelog" | "/claude-code" | "/cline" - | "/cloud" | "/codebuddy" | "/codex" | "/copilot" @@ -583,6 +582,7 @@ export interface FileRouteTypes { | "/goose" | "/grok" | "/hermes" + | "/hub" | "/junie" | "/kilo" | "/kimi" @@ -622,7 +622,6 @@ export interface FileRouteTypes { | "/changelog" | "/claude-code" | "/cline" - | "/cloud" | "/codebuddy" | "/codex" | "/copilot" @@ -642,6 +641,7 @@ export interface FileRouteTypes { | "/goose" | "/grok" | "/hermes" + | "/hub" | "/junie" | "/kilo" | "/kimi" @@ -682,7 +682,6 @@ export interface FileRouteTypes { | "/changelog" | "/claude-code" | "/cline" - | "/cloud" | "/codebuddy" | "/codex" | "/copilot" @@ -703,6 +702,7 @@ export interface FileRouteTypes { | "/goose" | "/grok" | "/hermes" + | "/hub" | "/junie" | "/kilo" | "/kimi" @@ -744,7 +744,6 @@ export interface RootRouteChildren { ChangelogRoute: typeof ChangelogRoute; ClaudeCodeRoute: typeof ClaudeCodeRoute; ClineRoute: typeof ClineRoute; - CloudRoute: typeof CloudRoute; CodebuddyRoute: typeof CodebuddyRoute; CodexRoute: typeof CodexRoute; CopilotRoute: typeof CopilotRoute; @@ -765,6 +764,7 @@ export interface RootRouteChildren { GooseRoute: typeof GooseRoute; GrokRoute: typeof GrokRoute; HermesRoute: typeof HermesRoute; + HubRoute: typeof HubRoute; JunieRoute: typeof JunieRoute; KiloRoute: typeof KiloRoute; KimiRoute: typeof KimiRoute; @@ -912,6 +912,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof JunieRouteImport; parentRoute: typeof rootRouteImport; }; + "/hub": { + id: "/hub"; + path: "/hub"; + fullPath: "/hub"; + preLoaderRoute: typeof HubRouteImport; + parentRoute: typeof rootRouteImport; + }; "/hermes": { id: "/hermes"; path: "/hermes"; @@ -1052,13 +1059,6 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof CodebuddyRouteImport; parentRoute: typeof rootRouteImport; }; - "/cloud": { - id: "/cloud"; - path: "/cloud"; - fullPath: "/cloud"; - preLoaderRoute: typeof CloudRouteImport; - parentRoute: typeof rootRouteImport; - }; "/cline": { id: "/cline"; path: "/cline"; @@ -1244,7 +1244,6 @@ const rootRouteChildren: RootRouteChildren = { ChangelogRoute: ChangelogRoute, ClaudeCodeRoute: ClaudeCodeRoute, ClineRoute: ClineRoute, - CloudRoute: CloudRoute, CodebuddyRoute: CodebuddyRoute, CodexRoute: CodexRoute, CopilotRoute: CopilotRoute, @@ -1265,6 +1264,7 @@ const rootRouteChildren: RootRouteChildren = { GooseRoute: GooseRoute, GrokRoute: GrokRoute, HermesRoute: HermesRoute, + HubRoute: HubRoute, JunieRoute: JunieRoute, KiloRoute: KiloRoute, KimiRoute: KimiRoute, diff --git a/packages/website/src/routes/cloud.tsx b/packages/website/src/routes/cloud.tsx deleted file mode 100644 index ee9a3b18c..000000000 --- a/packages/website/src/routes/cloud.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { useCallback, useState, type FormEvent } from "react"; -import { submitCloudSignup, type CloudSignupInput } from "~/cloud-signup"; -import { FAQItem } from "~/components/faq-item"; -import { SiteShell } from "~/components/site-shell"; -import { pageMeta } from "~/meta"; - -export const Route = createFileRoute("/cloud")({ - head: () => - pageMeta( - "Paseo Cloud - Design Partners", - "Run Paseo across machines, with a team, or inside a company. We're onboarding design partners for the hosted, multi-machine, multi-user version of Paseo.", - "/cloud", - ), - component: Cloud, -}); - -const INPUT_CLASS = - "block w-full rounded-md bg-white/5 border border-white/10 px-3.5 py-2.5 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-white/30 transition-colors"; - -type Status = "idle" | "submitting" | "success" | "error"; - -function Cloud() { - return ( - -

Paseo Cloud

-

- For using Paseo across machines, with a team, or inside a company. Looking for design - partners. -

- -
- - -
-
- ); -} - -function SignupForm() { - const [status, setStatus] = useState("idle"); - const [error, setError] = useState(null); - - const handleSubmit = useCallback(async (event: FormEvent) => { - event.preventDefault(); - setStatus("submitting"); - setError(null); - - const form = new FormData(event.currentTarget); - const data: CloudSignupInput = { - email: String(form.get("email") ?? ""), - name: form.get("name") ? String(form.get("name")) : undefined, - company: form.get("company") ? String(form.get("company")) : undefined, - role: form.get("role") ? String(form.get("role")) : undefined, - message: String(form.get("message") ?? ""), - honeypot: form.get("website") ? String(form.get("website")) : "", - }; - - try { - await submitCloudSignup({ data }); - setStatus("success"); - } catch (err) { - setStatus("error"); - setError(err instanceof Error ? err.message : "Something went wrong."); - } - }, []); - - if (status === "success") { - return ( -
-

- Got it. I'll be in touch. If you don't hear back within a week, ping me on{" "} - - Discord - - . -

-
- ); - } - - const submitting = status === "submitting"; - - return ( -
-
- - - - - - - - - - - - -
- - -