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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-26 07:14:44 +00:00
parent 09b5d1a3fa
commit b218bf3a86
2 changed files with 122 additions and 0 deletions

120
modules/rsocials/mod.ts Normal file
View File

@ -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: `<style>
main.rsocials-frame { padding: 0 !important; margin: 0; }
.rsocials-iframe {
width: 100%; border: none;
height: calc(100vh - 57px);
display: block;
}
</style>`,
body: `<main class="rsocials-frame">
<iframe class="rsocials-iframe" src="${src}" allow="clipboard-write"></iframe>
</main>`,
});
}
// /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",
};

View File

@ -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;