diff --git a/apps/marketing/README.md b/apps/marketing/README.md index 8e4a4b303c47b..8ac631b7661f7 100644 --- a/apps/marketing/README.md +++ b/apps/marketing/README.md @@ -7,7 +7,8 @@ are upstream's. - `vp run dev:marketing` serves it on port 4173. - `vp run build:marketing` writes `dist/`. - Deployed by Vercel from this directory; `vercel.ts` carries the install and build commands, - the headers for `/install.sh`, and the rewrite that shows thread prompt links on `/new`. The Vercel project needs Root Directory `apps/marketing` with + the headers for `/install.sh`, and the rewrites that show thread prompt links on `/new` and + copied thread links on `/threads`. The Vercel project needs Root Directory `apps/marketing` with files outside the root included, because the build reads `FORK.md` and stages `scripts/install.sh` from the repository root. diff --git a/apps/marketing/src/components/ServerForm.astro b/apps/marketing/src/components/ServerForm.astro new file mode 100644 index 0000000000000..afe5f0f688bc9 --- /dev/null +++ b/apps/marketing/src/components/ServerForm.astro @@ -0,0 +1,26 @@ +--- +// The "Open on my server" form of a link page; `wireServerForm` in lib/serverForm.ts runs it. +interface Props { + label: string; +} + +const { label } = Astro.props; +--- + +
diff --git a/apps/marketing/src/lib/serverForm.ts b/apps/marketing/src/lib/serverForm.ts new file mode 100644 index 0000000000000..a466f59bd3718 --- /dev/null +++ b/apps/marketing/src/lib/serverForm.ts @@ -0,0 +1,21 @@ +import { serverPromptLinkUrl } from "./promptLink"; + +/** Wires a link page's "Open on my server" form to open `appPath` on the address entered. */ +export function wireServerForm(appPath: string) { + const form = document.getElementById("server-form") as HTMLFormElement; + const address = document.getElementById("server-address") as HTMLInputElement; + const addressError = document.getElementById("server-error") as HTMLElement; + address.addEventListener("input", () => { + addressError.hidden = true; + }); + form.addEventListener("submit", (event) => { + event.preventDefault(); + const target = serverPromptLinkUrl(address.value, appPath); + if (target === null) { + addressError.hidden = false; + address.focus(); + return; + } + window.location.assign(target); + }); +} diff --git a/apps/marketing/src/lib/threadLink.test.ts b/apps/marketing/src/lib/threadLink.test.ts new file mode 100644 index 0000000000000..6acaaf40a3249 --- /dev/null +++ b/apps/marketing/src/lib/threadLink.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { serverPromptLinkUrl } from "./promptLink"; +import { readThreadLinkPage } from "./threadLink"; + +describe("readThreadLinkPage", () => { + it("opens the thread in the app and on a server", () => { + const page = readThreadLinkPage("https://j5.codes/threads/env-1/thread_2/"); + expect(page).toEqual({ + ok: true, + value: { appPath: "/env-1/thread_2", deepLink: "j5code://threads/env-1/thread_2" }, + }); + expect(page.ok && serverPromptLinkUrl("localhost:3773", page.value.appPath)).toBe( + "http://localhost:3773/env-1/thread_2", + ); + }); + + it("keeps escaped ids as the link wrote them", () => { + const page = readThreadLinkPage("https://j5.codes/threads/env%2E1/thread%20two"); + expect(page.ok && page.value.deepLink).toBe("j5code://threads/env%2E1/thread%20two"); + }); + + it("refuses a link that doesn't name a thread", () => { + for (const href of [ + "https://j5.codes/threads", + "https://j5.codes/threads/env-1", + "https://j5.codes/threads/env-1/thread-1/extra", + "https://j5.codes/threads/env-1/%E0%A4", + "https://j5.codes/threads/%20/thread-1", + "https://j5.codes/threads/env.1/thread-1", + ]) { + expect(readThreadLinkPage(href)).toEqual({ + ok: false, + error: "This link doesn't name a thread.", + }); + } + }); +}); diff --git a/apps/marketing/src/lib/threadLink.ts b/apps/marketing/src/lib/threadLink.ts new file mode 100644 index 0000000000000..2b17af647482f --- /dev/null +++ b/apps/marketing/src/lib/threadLink.ts @@ -0,0 +1,31 @@ +import { type PromptLinkResult, promptLinkDeepLink } from "@t3tools/shared/promptLinks"; + +export interface ThreadLinkPage { + /** The thread's path in a J5 web app, `/It opens in J5 Code if you're connected to the server it runs on and can see the thread.
+ +