From ed4fc08f78e0e03cc5821acb6b4f49be85dd6fd1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 3 Aug 2026 14:08:17 +0200 Subject: [PATCH] feat(website): replace the Cloud page with Paseo Hub The Cloud page pitched a hosted multi-user product and collected emails through a signup form. The Hub is a self-hosted service in private beta and the only way in is a Discord DM, so the signup server function and its webhook are deleted rather than repointed. /cloud now 301s to /hub from the worker entry, which already owns the site's permanent path redirects, so old links and search results keep working. The homepage also drops split panels, service proxy, voice, and keyboard shortcuts. They had turned the page into a feature list; it should show what Paseo enables at a high level. Brand and provider icons move into shared modules so the Hub page, the site header, and the landing page each use one copy instead of three. --- packages/website/src/cloud-signup.ts | 84 --- .../website/src/components/agent-icons.tsx | 84 +++ .../website/src/components/brand-icons.tsx | 46 ++ .../website/src/components/landing-page.tsx | 517 +++--------------- .../website/src/components/site-footer.tsx | 6 + .../website/src/components/site-header.tsx | 22 +- packages/website/src/llms.ts | 2 +- packages/website/src/routeTree.gen.ts | 42 +- packages/website/src/routes/cloud.tsx | 216 -------- packages/website/src/routes/hub.tsx | 449 +++++++++++++++ packages/website/src/server-entry.ts | 5 + packages/website/vite.config.ts | 4 +- 12 files changed, 680 insertions(+), 797 deletions(-) delete mode 100644 packages/website/src/cloud-signup.ts create mode 100644 packages/website/src/components/agent-icons.tsx create mode 100644 packages/website/src/components/brand-icons.tsx delete mode 100644 packages/website/src/routes/cloud.tsx create mode 100644 packages/website/src/routes/hub.tsx 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 ( -
-
- - - - - - - - - - - - -
- - -