From b218bf3a862f98ea890f45ee83ee8d847ce33110 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 26 Feb 2026 07:14:44 +0000 Subject: [PATCH] feat: add rSocials module with campaign builder proxy Registers rsocials module in rSpace unified system. Embeds the campaign strategy builder from rsocials:3000 via iframe in the rSpace shell, with API proxy for campaign CRUD. Accessible at /{space}/rsocials/campaign. Co-Authored-By: Claude Opus 4.6 --- modules/rsocials/mod.ts | 120 ++++++++++++++++++++++++++++++++++++++++ server/index.ts | 2 + 2 files changed, 122 insertions(+) create mode 100644 modules/rsocials/mod.ts diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts new file mode 100644 index 0000000..f5bb6bd --- /dev/null +++ b/modules/rsocials/mod.ts @@ -0,0 +1,120 @@ +/** + * rSocials module — campaign strategy workflow builder. + * + * Proxies campaign API + embeds the Next.js campaign editor from rsocials:3000. + * Page routes render an iframe inside the rSpace shell so the campaign builder + * gets the full rSpace header/nav while the Next.js app runs independently. + */ + +import { Hono } from "hono"; +import { renderShell } from "../../server/shell"; +import type { RSpaceModule } from "../../shared/module"; +import { getModuleInfoList } from "../../shared/module"; + +const RSOCIALS_INTERNAL = process.env.RSOCIALS_URL || "http://rsocials:3000"; +const RSOCIALS_PUBLIC = "https://rsocials.online"; + +const routes = new Hono(); + +// ── API proxy ──────────────────────────────────────────── +// Proxy all campaign API calls to the rsocials container + +routes.get("/api/campaigns", async (c) => { + const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns`); + return c.json(await res.json(), res.status as StatusCode); +}); + +routes.get("/api/campaigns/:id", async (c) => { + const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns/${c.req.param("id")}`); + return c.json(await res.json(), res.status as StatusCode); +}); + +routes.post("/api/campaigns", async (c) => { + const body = await c.req.text(); + const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); + return c.json(await res.json(), res.status as StatusCode); +}); + +routes.put("/api/campaigns/:id", async (c) => { + const body = await c.req.text(); + const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns/${c.req.param("id")}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body, + }); + return c.json(await res.json(), res.status as StatusCode); +}); + +routes.delete("/api/campaigns/:id", async (c) => { + const res = await fetch(`${RSOCIALS_INTERNAL}/api/campaigns/${c.req.param("id")}`, { + method: "DELETE", + }); + return c.json(await res.json(), res.status as StatusCode); +}); + +// ── Page routes ────────────────────────────────────────── + +function campaignFrame(src: string, space: string, title: string) { + return renderShell({ + title, + moduleId: "rsocials", + spaceSlug: space, + modules: getModuleInfoList(), + theme: "dark", + styles: ``, + body: `
+ +
`, + }); +} + +// /rsocials/ → campaign list +routes.get("/", (c) => { + const space = c.req.param("space") || "demo"; + return c.html( + campaignFrame(`${RSOCIALS_PUBLIC}/campaigns`, space, `Campaigns | rSocials`) + ); +}); + +// /rsocials/campaign → campaign list (alias) +routes.get("/campaign", (c) => { + const space = c.req.param("space") || "demo"; + return c.html( + campaignFrame(`${RSOCIALS_PUBLIC}/campaigns`, space, `Campaigns | rSocials`) + ); +}); + +// /rsocials/campaign/:id → specific campaign editor +routes.get("/campaign/:id", (c) => { + const space = c.req.param("space") || "demo"; + const id = c.req.param("id"); + return c.html( + campaignFrame( + `${RSOCIALS_PUBLIC}/campaigns/${id}`, + space, + `Campaign Editor | rSocials` + ) + ); +}); + +type StatusCode = 200 | 201 | 204 | 400 | 401 | 403 | 404 | 409 | 500 | 502; + +export const rsocialsModule: RSpaceModule = { + id: "rsocials", + name: "rSocials", + icon: "\uD83D\uDCE2", + description: "Campaign strategy workflow builder", + routes, + standaloneDomain: "rsocials.online", +}; diff --git a/server/index.ts b/server/index.ts index eac52ac..4b318ef 100644 --- a/server/index.ts +++ b/server/index.ts @@ -63,6 +63,7 @@ import { inboxModule } from "../modules/inbox/mod"; import { dataModule } from "../modules/data/mod"; import { splatModule } from "../modules/splat/mod"; import { photosModule } from "../modules/photos/mod"; +import { rsocialsModule } from "../modules/rsocials/mod"; import { spaces } from "./spaces"; import { renderShell, renderModuleLanding } from "./shell"; import { syncServer } from "./sync-instance"; @@ -92,6 +93,7 @@ registerModule(inboxModule); registerModule(dataModule); registerModule(splatModule); registerModule(photosModule); +registerModule(rsocialsModule); // ── Config ── const PORT = Number(process.env.PORT) || 3000;