diff --git a/apps/pwa/src/lib/moshpit-build-promo.mjs b/apps/pwa/src/lib/moshpit-build-promo.mjs new file mode 100644 index 00000000..9164c2a7 --- /dev/null +++ b/apps/pwa/src/lib/moshpit-build-promo.mjs @@ -0,0 +1,65 @@ +// The build-and-host offer, drawn on /pit. +// +// Somebody who has just bought a name has a name and nothing at it. The +// hosting help explains how to point it at a server of their own; this is the +// other answer, for everyone without one: describe the app, chovy.com builds it +// on the Profullstack stack and keeps it online. The storefront is +// pit.world/build; the form here posts straight to the same intake it does, so +// one click from the pit skips a page. +import { esc } from "./html.mjs"; + +export const BUILD_URL = "https://pit.world/build"; +export const BUILD_START_URL = "https://chovy.com/start"; +export const BUILD_PRICE_USD = 49; +export const HOSTING_PRICE_USD = 2; + +/** + * The name a `?ok=` banner just handed over, when it handed over exactly one. + * + * The buy and claim flows redirect with `ok= is yours.`; that sentence is + * the only record on the page of what was bought, so the offer reads it rather + * than guessing at the newest row. + */ +export function boughtName(ok) { + const m = /^([a-z0-9-]+\.[a-z0-9-]+) is yours\.?$/i.exec(String(ok ?? "").trim()); + return m ? m[1].toLowerCase() : null; +} + +/** + * The card. `name` is the name just bought, when there is one: the offer then + * talks about that name and seeds the idea with it. + */ +export function buildPromo({ name = null } = {}) { + const forName = name ? ` for ${esc(name)}` : ""; + const seed = name ? `A site at ${name}: ` : ""; + return `
+

no server? no problem

+

We build it${forName}. We host it.

+

Describe the app in plain words. An AI product team writes a plan you can argue + with, builds it on the open source Profullstack + stack, and shows you it working before anything launches. + $${BUILD_PRICE_USD} to build, including the first 30 days, then $${HOSTING_PRICE_USD} a month + to keep it online. Paid in crypto through CoinPay.

+
+ + + +
+ + how build + hosting works +
+
+
`; +} + +export const BUILD_PROMO_CSS = ` +.pit-build{border:1px solid var(--acid);border-radius:var(--r);background:color-mix(in srgb,var(--acid) 6%,var(--surface)); + padding:18px 20px;margin:18px 0;max-width:72ch} +.pit-build h2{font-size:clamp(1.2rem,3.4vw,1.6rem);margin:4px 0 8px} +.pit-build b{color:var(--acid);font-weight:600} +.pit-build-form textarea{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text); + border:1px solid var(--line-2);border-radius:8px;padding:10px 12px;font:inherit;font-size:.9rem;margin-top:10px;resize:vertical} +.pit-build-form textarea:focus{border-color:var(--acid);outline:0} +`; diff --git a/apps/pwa/src/routes/moshpit.mjs b/apps/pwa/src/routes/moshpit.mjs index d1c3796d..9e1bf196 100644 --- a/apps/pwa/src/routes/moshpit.mjs +++ b/apps/pwa/src/routes/moshpit.mjs @@ -42,6 +42,7 @@ import { bearer, userForApiKey } from "../lib/apikey.mjs"; import { balance } from "../lib/credits.mjs"; import { resolverConfig } from "../lib/moshpit-resolvers.mjs"; import { landingFor } from "../lib/moshpit-landing.mjs"; +import { buildPromo, boughtName, BUILD_PROMO_CSS, BUILD_URL, BUILD_PRICE_USD, HOSTING_PRICE_USD } from "../lib/moshpit-build-promo.mjs"; import { FEED_KINDS, loadFeed } from "../lib/feed.mjs"; import { sendOfferAnswer, @@ -2707,7 +2708,8 @@ const landingCard = (req, landing) => { return `

you asked for ${esc(landing.name)}

${name} is already yours.

-

It is in your list below — change where it points, or release it.

+

It is in your list below — change where it points, or release it. + Nothing at it yet? We build and host it — $${BUILD_PRICE_USD}, then $${HOSTING_PRICE_USD} a month.

`; } @@ -3123,6 +3125,8 @@ function hostingHelp() { moshcode template install bun-caddy-sqlite writes the Caddyfile, the systemd units and a Bun + SQLite service already wired up this way. moshcode template list shows what there is.

+

No server, or no time? We build the + app and host it for you — $${BUILD_PRICE_USD} to build, then $${HOSTING_PRICE_USD} a month.

Check it with moshcode dns resolve seo.rank, then curl -6 http://seo.rank/. A port other than 80 only works in the /n/ view — DNS carries an address and has nowhere to put a port, so the browser @@ -3390,7 +3394,7 @@ moshpitRouter.get("/pit", async (req, res) => { res.type("html").send(page({ title: "moshcode ▸ the pit", - head: ``, + head: ``, body: `${appBar(req.user, bal, req.csrfToken)}

the moshpit namespace

@@ -3410,6 +3414,7 @@ moshpitRouter.get("/pit", async (req, res) => {

${landingCard(req, landing)} ${msg} + ${buildPromo({ name: boughtName(req.query.ok) })} ${req.user ? claimForm(req) + bulkClaimForm(req) : ""} ${pitTabs(tab, { yours: mineTotal, theirs: theirsTotal, forSale: filter ? 0 : forSaleCount }, filter?.query ?? "")} ${filterBox(tab, filter?.query ?? "", req.user ? (tab === "theirs" ? "theirs" : "mine") : "all")} diff --git a/apps/pwa/test/moshpit-build-promo.test.mjs b/apps/pwa/test/moshpit-build-promo.test.mjs new file mode 100644 index 00000000..a1d5e33d --- /dev/null +++ b/apps/pwa/test/moshpit-build-promo.test.mjs @@ -0,0 +1,31 @@ +// The build-and-host offer on /pit: it must reach the paid intake, and it must +// never echo a banner it was handed into the page unescaped. +import assert from "node:assert/strict"; +import test from "node:test"; +import { boughtName, buildPromo, BUILD_START_URL } from "../src/lib/moshpit-build-promo.mjs"; + +test("boughtName reads the name a buy or claim just handed over", () => { + assert.equal(boughtName("pay.agent is yours."), "pay.agent"); + assert.equal(boughtName("Pay.Agent is yours"), "pay.agent"); + // An ending, a count, or anything else is not one name. + assert.equal(boughtName(".agent is yours."), null); + assert.equal(boughtName("3 names claimed"), null); + assert.equal(boughtName(undefined), null); +}); + +test("the form posts to the chovy Starter intake, tagged as moshpit", () => { + const html = buildPromo(); + assert.match(html, new RegExp(`action="${BUILD_START_URL}" method="get"`)); + assert.match(html, /name="plan" value="starter"/); + assert.match(html, /name="ref" value="moshpit"/); + assert.match(html, /