From 5408eb03761496ae2fcb99a4d8db3b845bfa09c5 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 1 Mar 2026 13:55:29 -0800 Subject: [PATCH] feat: add outputPaths to module interface and browsable list pages Add OutputPath type to RSpaceModule so each module declares what content types it produces (e.g. notebooks, routes, campaigns). Auto-generate browsable list pages at /:space/:moduleId/:path that render a card grid inside the standard shell, fetching items from the module's API. Declares outputPaths across 23 modules (rwallet/rinbox skipped). Move campaign demo from standalone campaign-demo space to /rsocials/campaign route with a dedicated timeline view and /api/campaigns endpoint. Co-Authored-By: Claude Opus 4.6 --- modules/rbooks/mod.ts | 4 + modules/rcart/mod.ts | 4 + modules/rchoices/mod.ts | 4 + modules/rdata/mod.ts | 4 + modules/rdesign/mod.ts | 4 + modules/rdocs/mod.ts | 4 + modules/rfiles/mod.ts | 4 + modules/rforum/mod.ts | 4 + modules/rfunds/mod.ts | 4 + modules/rmaps/mod.ts | 4 + modules/rnetwork/mod.ts | 4 + modules/rnotes/mod.ts | 5 + modules/rphotos/mod.ts | 4 + modules/rpubs/mod.ts | 4 + modules/rsocials/campaign-data.ts | 172 ++++++++++++++++++++++++++++++ modules/rsocials/mod.ts | 132 ++++++++++++++++++++++- modules/rspace/mod.ts | 3 + modules/rsplat/mod.ts | 3 + modules/rswag/mod.ts | 3 + modules/rtrips/mod.ts | 3 + modules/rtube/mod.ts | 4 + modules/rvote/mod.ts | 4 + modules/rwork/mod.ts | 4 + server/index.ts | 30 +++--- server/output-list.ts | 143 +++++++++++++++++++++++++ 25 files changed, 540 insertions(+), 18 deletions(-) create mode 100644 modules/rsocials/campaign-data.ts create mode 100644 server/output-list.ts diff --git a/modules/rbooks/mod.ts b/modules/rbooks/mod.ts index 614a841..ca9e624 100644 --- a/modules/rbooks/mod.ts +++ b/modules/rbooks/mod.ts @@ -319,6 +319,10 @@ export const booksModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "resource"], + outputPaths: [ + { path: "books", name: "Books", icon: "๐Ÿ“š", description: "Community PDF library" }, + { path: "collections", name: "Collections", icon: "๐Ÿ“‘", description: "Curated book collections" }, + ], async onSpaceCreate(spaceSlug: string) { // Books are global, not space-scoped (for now). No-op. diff --git a/modules/rcart/mod.ts b/modules/rcart/mod.ts index 3ef01d7..b38d413 100644 --- a/modules/rcart/mod.ts +++ b/modules/rcart/mod.ts @@ -493,4 +493,8 @@ export const cartModule: RSpaceModule = { }, ], acceptsFeeds: ["economic", "data"], + outputPaths: [ + { path: "products", name: "Products", icon: "๐Ÿ›๏ธ", description: "Print-on-demand product catalog" }, + { path: "orders", name: "Orders", icon: "๐Ÿ“ฆ", description: "Order history and fulfillment tracking" }, + ], }; diff --git a/modules/rchoices/mod.ts b/modules/rchoices/mod.ts index dcd8d70..3bf430b 100644 --- a/modules/rchoices/mod.ts +++ b/modules/rchoices/mod.ts @@ -79,4 +79,8 @@ export const choicesModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "governance"], + outputPaths: [ + { path: "polls", name: "Polls", icon: "โ˜‘๏ธ", description: "Active and closed polls" }, + { path: "results", name: "Results", icon: "๐Ÿ“Š", description: "Poll and scoring results" }, + ], }; diff --git a/modules/rdata/mod.ts b/modules/rdata/mod.ts index d24c404..af080d0 100644 --- a/modules/rdata/mod.ts +++ b/modules/rdata/mod.ts @@ -157,4 +157,8 @@ export const dataModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "economic"], + outputPaths: [ + { path: "datasets", name: "Datasets", icon: "๐Ÿ“Š", description: "Collected analytics datasets" }, + { path: "dashboards", name: "Dashboards", icon: "๐Ÿ“ˆ", description: "Analytics dashboards and reports" }, + ], }; diff --git a/modules/rdesign/mod.ts b/modules/rdesign/mod.ts index bbe7b8a..c08faf2 100644 --- a/modules/rdesign/mod.ts +++ b/modules/rdesign/mod.ts @@ -57,4 +57,8 @@ export const designModule: RSpaceModule = { routes, standaloneDomain: "rdesign.online", externalApp: { url: AFFINE_URL, name: "Affine" }, + outputPaths: [ + { path: "designs", name: "Designs", icon: "๐ŸŽฏ", description: "Design files and mockups" }, + { path: "templates", name: "Templates", icon: "๐Ÿ“", description: "Reusable design templates" }, + ], }; diff --git a/modules/rdocs/mod.ts b/modules/rdocs/mod.ts index 10906ac..2d2345d 100644 --- a/modules/rdocs/mod.ts +++ b/modules/rdocs/mod.ts @@ -57,4 +57,8 @@ export const docsModule: RSpaceModule = { routes, standaloneDomain: "rdocs.online", externalApp: { url: DOCMOST_URL, name: "Docmost" }, + outputPaths: [ + { path: "documents", name: "Documents", icon: "๐Ÿ“", description: "Collaborative documents" }, + { path: "wikis", name: "Wikis", icon: "๐Ÿ“–", description: "Knowledge base wikis" }, + ], }; diff --git a/modules/rfiles/mod.ts b/modules/rfiles/mod.ts index 1f8d80f..6306a06 100644 --- a/modules/rfiles/mod.ts +++ b/modules/rfiles/mod.ts @@ -419,4 +419,8 @@ export const filesModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "resource"], + outputPaths: [ + { path: "files", name: "Files", icon: "๐Ÿ“", description: "Uploaded files and documents" }, + { path: "shares", name: "Shares", icon: "๐Ÿ”—", description: "Public share links" }, + ], }; diff --git a/modules/rforum/mod.ts b/modules/rforum/mod.ts index 323fd54..dfc7c76 100644 --- a/modules/rforum/mod.ts +++ b/modules/rforum/mod.ts @@ -210,4 +210,8 @@ export const forumModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "governance"], + outputPaths: [ + { path: "threads", name: "Threads", icon: "๐Ÿ’ฌ", description: "Forum discussion threads" }, + { path: "categories", name: "Categories", icon: "๐Ÿ“‚", description: "Forum categories and topics" }, + ], }; diff --git a/modules/rfunds/mod.ts b/modules/rfunds/mod.ts index 4ae283a..96f667d 100644 --- a/modules/rfunds/mod.ts +++ b/modules/rfunds/mod.ts @@ -279,4 +279,8 @@ export const fundsModule: RSpaceModule = { }, ], acceptsFeeds: ["governance", "data"], + outputPaths: [ + { path: "budgets", name: "Budgets", icon: "๐Ÿ’ฐ", description: "Budget allocations and funnels" }, + { path: "flows", name: "Flows", icon: "๐ŸŒŠ", description: "Revenue and resource flow visualizations" }, + ], }; diff --git a/modules/rmaps/mod.ts b/modules/rmaps/mod.ts index 7badabe..86321c7 100644 --- a/modules/rmaps/mod.ts +++ b/modules/rmaps/mod.ts @@ -186,4 +186,8 @@ export const mapsModule: RSpaceModule = { }, ], acceptsFeeds: ["data"], + outputPaths: [ + { path: "routes", name: "Routes", icon: "๐Ÿ›ค๏ธ", description: "Saved navigation routes" }, + { path: "places", name: "Places", icon: "๐Ÿ“", description: "Saved locations and points of interest" }, + ], }; diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts index bc47502..1c66ed5 100644 --- a/modules/rnetwork/mod.ts +++ b/modules/rnetwork/mod.ts @@ -269,4 +269,8 @@ export const networkModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "trust", "governance"], + outputPaths: [ + { path: "connections", name: "Connections", icon: "๐Ÿค", description: "Community member connections" }, + { path: "groups", name: "Groups", icon: "๐Ÿ‘ฅ", description: "Relationship groups and circles" }, + ], }; diff --git a/modules/rnotes/mod.ts b/modules/rnotes/mod.ts index 70a9d7c..ac5dc1a 100644 --- a/modules/rnotes/mod.ts +++ b/modules/rnotes/mod.ts @@ -415,4 +415,9 @@ export const notesModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "resource"], + outputPaths: [ + { path: "notebooks", name: "Notebooks", icon: "๐Ÿ““", description: "Rich-text collaborative notebooks" }, + { path: "transcripts", name: "Transcripts", icon: "๐ŸŽ™๏ธ", description: "Voice transcription records" }, + { path: "articles", name: "Articles", icon: "๐Ÿ“ฐ", description: "Published articles and posts" }, + ], }; diff --git a/modules/rphotos/mod.ts b/modules/rphotos/mod.ts index c0ef75f..23e320d 100644 --- a/modules/rphotos/mod.ts +++ b/modules/rphotos/mod.ts @@ -140,4 +140,8 @@ export const photosModule: RSpaceModule = { }, ], acceptsFeeds: ["data"], + outputPaths: [ + { path: "albums", name: "Albums", icon: "๐Ÿ“ธ", description: "Photo albums and collections" }, + { path: "galleries", name: "Galleries", icon: "๐Ÿ–ผ๏ธ", description: "Public photo galleries" }, + ], }; diff --git a/modules/rpubs/mod.ts b/modules/rpubs/mod.ts index 1205bfb..71ebc5c 100644 --- a/modules/rpubs/mod.ts +++ b/modules/rpubs/mod.ts @@ -360,4 +360,8 @@ export const pubsModule: RSpaceModule = { }, ], acceptsFeeds: ["data"], + outputPaths: [ + { path: "publications", name: "Publications", icon: "๐Ÿ“–", description: "Published pocket books and zines" }, + { path: "drafts", name: "Drafts", icon: "๐Ÿ“", description: "Work-in-progress documents" }, + ], }; diff --git a/modules/rsocials/campaign-data.ts b/modules/rsocials/campaign-data.ts new file mode 100644 index 0000000..e4eaf16 --- /dev/null +++ b/modules/rsocials/campaign-data.ts @@ -0,0 +1,172 @@ +/** + * Campaign demo data โ€” "MycoFi Earth Launch" campaign. + * + * Extracted from server/seed-campaign.ts. This data drives the + * /campaign page in rSocials and the /api/campaigns endpoint. + */ + +export interface CampaignPost { + id: string; + platform: string; + postType: string; + stepNumber: number; + content: string; + scheduledAt: string; + status: string; + hashtags: string[]; + phase: number; + phaseLabel: string; +} + +export interface Campaign { + id: string; + title: string; + description: string; + duration: string; + platforms: string[]; + phases: { name: string; label: string; days: string }[]; + posts: CampaignPost[]; +} + +const PLATFORM_ICONS: Record = { + x: "๐•", + linkedin: "in", + instagram: "๐Ÿ“ท", + youtube: "โ–ถ๏ธ", + threads: "๐Ÿงต", + bluesky: "๐Ÿฆ‹", +}; + +const PLATFORM_COLORS: Record = { + x: "#000000", + linkedin: "#0A66C2", + instagram: "#E4405F", + youtube: "#FF0000", + threads: "#000000", + bluesky: "#0085FF", +}; + +export { PLATFORM_ICONS, PLATFORM_COLORS }; + +export const MYCOFI_CAMPAIGN: Campaign = { + id: "mycofi-earth-launch", + title: "MycoFi Earth Launch Campaign", + description: "Multi-platform product launch campaign for MycoFi Earth โ€” a regenerative finance platform modeled on mycelial networks.", + duration: "Feb 21โ€“25, 2026 (5 days)", + platforms: ["X", "LinkedIn", "Instagram", "YouTube", "Threads", "Bluesky"], + phases: [ + { name: "pre-launch", label: "Pre-Launch Hype", days: "Day -3 to -1" }, + { name: "launch", label: "Launch Day", days: "Day 0" }, + { name: "amplification", label: "Amplification", days: "Day +1" }, + ], + posts: [ + { + id: "post-x-teaser", + platform: "x", + postType: "thread", + stepNumber: 1, + content: "Something is growing in the mycelium... ๐Ÿ„\n\nFor the past 2 years, we've been building the infrastructure for a regenerative economy.\n\nOn Feb 24, we reveal everything.\n\nA thread on why the old financial system is composting itself ๐Ÿงต๐Ÿ‘‡", + scheduledAt: "2026-02-21T09:00:00", + status: "scheduled", + hashtags: ["MycoFi", "RegenFinance", "Web3", "ComingSoon"], + phase: 1, + phaseLabel: "Pre-Launch Hype", + }, + { + id: "post-linkedin-thought", + platform: "linkedin", + postType: "article", + stepNumber: 2, + content: "The regenerative finance movement isn't just about returns โ€” it's about redesigning incentive structures from the ground up.\n\nIn this article, I break down why mycelial network theory offers the best model for decentralized economic coordination.\n\n3 key insights from 2 years of building MycoFi Earth...", + scheduledAt: "2026-02-22T11:00:00", + status: "scheduled", + hashtags: ["RegenerativeFinance", "DeFi", "SystemsThinking", "Leadership"], + phase: 1, + phaseLabel: "Pre-Launch Hype", + }, + { + id: "post-ig-carousel", + platform: "instagram", + postType: "carousel", + stepNumber: 3, + content: "5 Ways Mycelium Networks Mirror the Future of Finance ๐ŸŒ๐Ÿ„\n\nSlide 1: The problem with extractive finance\nSlide 2: How mycelium redistributes nutrients\nSlide 3: Token-weighted funding circles\nSlide 4: Community governance that actually works\nSlide 5: Join the launch โ€” Feb 24", + scheduledAt: "2026-02-23T14:00:00", + status: "scheduled", + hashtags: ["MycoFi", "RegenerativeEconomy", "Infographic", "Web3Education"], + phase: 1, + phaseLabel: "Pre-Launch Hype", + }, + { + id: "post-yt-launch", + platform: "youtube", + postType: "video", + stepNumber: 4, + content: "MycoFi Earth โ€” Official Launch Video\n\nThe regenerative economy starts here. Watch how mycelial intelligence is reshaping finance, governance, and community coordination.\n\nFeaturing interviews with 12 builders from the ecosystem.\n\n[18:42]", + scheduledAt: "2026-02-24T10:00:00", + status: "draft", + hashtags: ["MycoFiLaunch", "RegenerativeFinance", "Documentary", "Web3"], + phase: 2, + phaseLabel: "Launch Day", + }, + { + id: "post-x-launch", + platform: "x", + postType: "thread", + stepNumber: 5, + content: "๐Ÿ„ MycoFi Earth is LIVE ๐Ÿ„\n\nAfter 2 years of building, the regenerative finance platform is here.\n\nWhat is it?\nโ€ข Token-weighted funding circles\nโ€ข Mycelial governance (no whales)\nโ€ข Composting mechanism for failed proposals\nโ€ข 100% on-chain, 100% community-owned\n\n๐Ÿ‘‡ Full breakdown thread", + scheduledAt: "2026-02-24T10:15:00", + status: "draft", + hashtags: ["MycoFi", "Launch", "RegenFinance", "DeFi", "DAO"], + phase: 2, + phaseLabel: "Launch Day", + }, + { + id: "post-linkedin-launch", + platform: "linkedin", + postType: "text", + stepNumber: 6, + content: "Today we're launching MycoFi Earth โ€” a regenerative finance platform modeled on mycelial networks.\n\nWhy it matters for the future of organizational design:\n\n1. Composting mechanism: Failed proposals return resources to the network\n2. Nutrient routing: Funds flow to where they're needed most\n3. No single point of failure: True decentralization\n\nFull video + docs in comments โ†“", + scheduledAt: "2026-02-24T11:00:00", + status: "draft", + hashtags: ["Launch", "RegenerativeFinance", "Innovation", "FutureOfWork"], + phase: 2, + phaseLabel: "Launch Day", + }, + { + id: "post-ig-reel", + platform: "instagram", + postType: "reel", + stepNumber: 7, + content: "60-second explainer: How MycoFi Earth works ๐Ÿ„โœจ\n\nVisual breakdown of the token flow from contributor โ†’ funding circle โ†’ project โ†’ compost.\n\nSet to lo-fi beats with mycelium time-lapse footage.\n\nCTA: Link in bio to join the first funding circle.", + scheduledAt: "2026-02-25T12:00:00", + status: "draft", + hashtags: ["MycoFi", "RegenFinance", "Explainer", "Web3", "Reels"], + phase: 3, + phaseLabel: "Amplification", + }, + { + id: "post-threads-xpost", + platform: "threads", + postType: "text", + stepNumber: 8, + content: "We just launched MycoFi Earth and the response has been incredible ๐ŸŒŸ\n\nThe idea is simple: what if finance worked like mycelium?\n\nMycelium doesn't hoard โ€” it redistributes. MycoFi applies that logic to community funding.\n\nEarly access is open. Come grow with us ๐ŸŒฑ", + scheduledAt: "2026-02-25T14:00:00", + status: "draft", + hashtags: ["MycoFi", "RegenEconomy", "Community"], + phase: 3, + phaseLabel: "Amplification", + }, + { + id: "post-bluesky-launch", + platform: "bluesky", + postType: "text", + stepNumber: 9, + content: "MycoFi Earth just went live ๐Ÿ„\n\nIt's a regenerative finance platform where funding flows like nutrients through a mycelial network.\n\nNo VCs. No whales. Just communities funding what matters.\n\nmycofi.earth", + scheduledAt: "2026-02-25T15:00:00", + status: "draft", + hashtags: ["MycoFi", "RegenFinance", "Bluesky"], + phase: 3, + phaseLabel: "Amplification", + }, + ], +}; diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index a806c6d..9a3acce 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -6,10 +6,11 @@ */ import { Hono } from "hono"; -import { renderShell, renderExternalAppShell } from "../../server/shell"; +import { renderShell, renderExternalAppShell, escapeHtml } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { renderLanding } from "./landing"; +import { MYCOFI_CAMPAIGN, PLATFORM_ICONS, PLATFORM_COLORS } from "./campaign-data"; const routes = new Hono(); @@ -73,6 +74,26 @@ routes.get("/api/feed", (c) => { }); }); +// โ”€โ”€ API: Campaigns list โ”€โ”€ +routes.get("/api/campaigns", (c) => { + const space = c.req.param("space") || "demo"; + const campaign = MYCOFI_CAMPAIGN; + return c.json({ + campaigns: [ + { + id: campaign.id, + title: campaign.title, + description: campaign.description, + duration: campaign.duration, + platforms: campaign.platforms, + postCount: campaign.posts.length, + updated_at: "2026-02-21T09:00:00Z", + url: `/${space}/rsocials/campaign`, + }, + ], + }); +}); + // โ”€โ”€ Demo feed data (server-rendered, no API calls) โ”€โ”€ const DEMO_FEED = [ { @@ -166,6 +187,111 @@ function renderDemoFeedHTML(): string { `; } +// โ”€โ”€ Campaign page route โ”€โ”€ +function renderCampaignPage(space: string): string { + const c = MYCOFI_CAMPAIGN; + const phases = [1, 2, 3]; + const phaseIcons = ["๐Ÿ“ฃ", "๐Ÿš€", "๐Ÿ“ก"]; + + const phaseHTML = phases.map((phaseNum, i) => { + const phasePosts = c.posts.filter((p) => p.phase === phaseNum); + const phaseInfo = c.phases[i]; + const postsHTML = phasePosts.map((post) => { + const icon = PLATFORM_ICONS[post.platform] || post.platform; + const color = PLATFORM_COLORS[post.platform] || "#64748b"; + const statusClass = post.status === "scheduled" ? "campaign-status--scheduled" : "campaign-status--draft"; + const date = new Date(post.scheduledAt); + const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); + const contentPreview = escapeHtml(post.content.length > 180 ? post.content.substring(0, 180) + "..." : post.content); + const tags = post.hashtags.map((h) => `#${escapeHtml(h)}`).join(" "); + + return ` +
+
+ ${icon} + + ${escapeHtml(post.status)} +
+
Step ${post.stepNumber}
+

${contentPreview.replace(/\n/g, "
")}

+ +
`; + }).join("\n"); + + return ` +
+

${phaseIcons[i]} Phase ${phaseNum}: ${escapeHtml(phaseInfo.label)} ${escapeHtml(phaseInfo.days)}

+
${postsHTML}
+
`; + }).join("\n"); + + return ` +
+
+ ๐Ÿ„ +
+

${escapeHtml(c.title)}

+

${escapeHtml(c.description)}

+
+ ๐Ÿ“… ${escapeHtml(c.duration)} + ๐Ÿ“ฑ ${c.platforms.join(", ")} + ๐Ÿ“ ${c.posts.length} posts across ${c.phases.length} phases +
+
+
+ ${phaseHTML} +
`; +} + +const CAMPAIGN_CSS = ` +.campaign-page { max-width: 900px; margin: 0 auto; padding: 2rem 1rem; } +.campaign-page__header { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #334155; } +.campaign-page__icon { font-size: 3rem; } +.campaign-page__title { margin: 0; font-size: 1.5rem; color: #f1f5f9; } +.campaign-page__desc { margin: 0.25rem 0 0.5rem; color: #94a3b8; font-size: 0.9rem; line-height: 1.5; } +.campaign-page__stats { display: flex; flex-wrap: wrap; gap: 1rem; font-size: 0.8rem; color: #64748b; } +.campaign-phase { margin-bottom: 2rem; } +.campaign-phase__title { font-size: 1.15rem; color: #e2e8f0; margin: 0 0 1rem; display: flex; align-items: center; gap: 0.5rem; } +.campaign-phase__days { font-size: 0.8rem; color: #64748b; font-weight: 400; } +.campaign-phase__posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; } +.campaign-post { + background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; padding: 1rem; + transition: border-color 0.15s; +} +.campaign-post:hover { border-color: #6366f1; } +.campaign-post__header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; } +.campaign-post__platform { + width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; + color: white; font-size: 0.75rem; font-weight: 700; flex-shrink: 0; +} +.campaign-post__meta { flex: 1; min-width: 0; } +.campaign-post__meta strong { display: block; font-size: 0.8rem; color: #e2e8f0; text-transform: capitalize; } +.campaign-post__date { font-size: 0.7rem; color: #64748b; } +.campaign-post__step { font-size: 0.65rem; color: #6366f1; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; } +.campaign-status { font-size: 0.6rem; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; font-weight: 600; letter-spacing: 0.05em; white-space: nowrap; } +.campaign-status--scheduled { background: rgba(16,185,129,0.15); color: #34d399; } +.campaign-status--draft { background: rgba(251,191,36,0.15); color: #fbbf24; } +.campaign-post__content { font-size: 0.8rem; color: #94a3b8; line-height: 1.5; margin: 0 0 0.5rem; } +.campaign-post__tags { display: flex; flex-wrap: wrap; gap: 0.25rem; } +.campaign-tag { font-size: 0.65rem; color: #7dd3fc; } +`; + +routes.get("/campaign", (c) => { + const space = c.req.param("space") || "demo"; + return c.html(renderShell({ + title: `Campaign โ€” rSocials | rSpace`, + moduleId: "rsocials", + spaceSlug: space, + modules: getModuleInfoList(), + theme: "dark", + body: renderCampaignPage(space), + styles: ``, + })); +}); + // โ”€โ”€ Page route โ”€โ”€ routes.get("/", (c) => { const space = c.req.param("space") || "demo"; @@ -337,4 +463,8 @@ export const socialsModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "trust"], + outputPaths: [ + { path: "campaigns", name: "Campaigns", icon: "๐Ÿ“ข", description: "Social media campaigns" }, + { path: "posts", name: "Posts", icon: "๐Ÿ“ฑ", description: "Social feed posts across platforms" }, + ], }; diff --git a/modules/rspace/mod.ts b/modules/rspace/mod.ts index 0026e1d..6020b1e 100644 --- a/modules/rspace/mod.ts +++ b/modules/rspace/mod.ts @@ -71,4 +71,7 @@ export const canvasModule: RSpaceModule = { }, ], acceptsFeeds: ["economic", "trust", "data", "attention", "governance", "resource"], + outputPaths: [ + { path: "canvases", name: "Canvases", icon: "๐ŸŽจ", description: "Collaborative infinite canvases" }, + ], }; diff --git a/modules/rsplat/mod.ts b/modules/rsplat/mod.ts index 1ce66b1..7d42e79 100644 --- a/modules/rsplat/mod.ts +++ b/modules/rsplat/mod.ts @@ -543,6 +543,9 @@ export const splatModule: RSpaceModule = { landingPage: renderLanding, standaloneDomain: "rsplat.online", hidden: true, + outputPaths: [ + { path: "drawings", name: "Drawings", icon: "๐Ÿ”ฎ", description: "3D Gaussian splat drawings" }, + ], async onSpaceCreate(_spaceSlug: string) { // Splats are scoped by space_slug column. No per-space setup needed. diff --git a/modules/rswag/mod.ts b/modules/rswag/mod.ts index 0c458e5..5f4ac01 100644 --- a/modules/rswag/mod.ts +++ b/modules/rswag/mod.ts @@ -266,4 +266,7 @@ export const swagModule: RSpaceModule = { }, ], acceptsFeeds: ["economic", "resource"], + outputPaths: [ + { path: "merchandise", name: "Merchandise", icon: "๐Ÿ‘•", description: "Print-ready swag designs" }, + ], }; diff --git a/modules/rtrips/mod.ts b/modules/rtrips/mod.ts index 15eae34..dc07612 100644 --- a/modules/rtrips/mod.ts +++ b/modules/rtrips/mod.ts @@ -305,4 +305,7 @@ export const tripsModule: RSpaceModule = { }, ], acceptsFeeds: ["economic", "data"], + outputPaths: [ + { path: "itineraries", name: "Itineraries", icon: "๐Ÿ—“๏ธ", description: "Trip itineraries with bookings and activities" }, + ], }; diff --git a/modules/rtube/mod.ts b/modules/rtube/mod.ts index 95ce15d..1427c80 100644 --- a/modules/rtube/mod.ts +++ b/modules/rtube/mod.ts @@ -242,4 +242,8 @@ export const tubeModule: RSpaceModule = { }, ], acceptsFeeds: ["data", "resource"], + outputPaths: [ + { path: "videos", name: "Videos", icon: "๐ŸŽฌ", description: "Hosted videos and recordings" }, + { path: "playlists", name: "Playlists", icon: "๐Ÿ“บ", description: "Video playlists and channels" }, + ], }; diff --git a/modules/rvote/mod.ts b/modules/rvote/mod.ts index 36aad62..87a7583 100644 --- a/modules/rvote/mod.ts +++ b/modules/rvote/mod.ts @@ -379,4 +379,8 @@ export const voteModule: RSpaceModule = { }, ], acceptsFeeds: ["economic", "data"], + outputPaths: [ + { path: "proposals", name: "Proposals", icon: "๐Ÿ“œ", description: "Governance proposals for conviction voting" }, + { path: "ballots", name: "Ballots", icon: "๐Ÿ—ณ๏ธ", description: "Voting ballots and results" }, + ], }; diff --git a/modules/rwork/mod.ts b/modules/rwork/mod.ts index 4ea1522..d586a1d 100644 --- a/modules/rwork/mod.ts +++ b/modules/rwork/mod.ts @@ -254,4 +254,8 @@ export const workModule: RSpaceModule = { }, ], acceptsFeeds: ["governance", "data"], + outputPaths: [ + { path: "projects", name: "Projects", icon: "๐Ÿ“‹", description: "Kanban project boards" }, + { path: "tasks", name: "Tasks", icon: "โœ…", description: "Task cards across all boards" }, + ], }; diff --git a/server/index.ts b/server/index.ts index 2b6f2f2..ca7f89d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -30,7 +30,7 @@ import { } from "./community-store"; import type { NestPermissions, SpaceRefFilter } from "./community-store"; import { ensureDemoCommunity } from "./seed-demo"; -import { ensureCampaignDemo } from "./seed-campaign"; +// Campaign demo moved to rsocials module โ€” see modules/rsocials/campaign-data.ts import type { SpaceVisibility } from "./community-store"; import { verifyEncryptIDToken, @@ -69,6 +69,7 @@ import { docsModule } from "../modules/rdocs/mod"; import { designModule } from "../modules/rdesign/mod"; import { spaces } from "./spaces"; import { renderShell, renderModuleLanding } from "./shell"; +import { renderOutputListPage } from "./output-list"; import { renderMainLanding, renderSpaceDashboard } from "./landing"; import { fetchLandingPage } from "./landing-proxy"; import { syncServer } from "./sync-instance"; @@ -394,21 +395,7 @@ app.post("/api/communities/demo/reset", async (c) => { return c.json({ ok: true, message: "Demo community reset to seed data" }); }); -// POST /api/communities/campaign-demo/reset -app.post("/api/communities/campaign-demo/reset", async (c) => { - const now = Date.now(); - if (now - lastDemoReset < DEMO_RESET_COOLDOWN) { - const remaining = Math.ceil((DEMO_RESET_COOLDOWN - (now - lastDemoReset)) / 1000); - return c.json({ error: `Reset on cooldown. Try again in ${remaining}s` }, 429); - } - lastDemoReset = now; - await loadCommunity("campaign-demo"); - clearShapes("campaign-demo"); - await ensureCampaignDemo(); - broadcastAutomergeSync("campaign-demo"); - broadcastJsonSnapshot("campaign-demo"); - return c.json({ ok: true, message: "Campaign demo reset to seed data" }); -}); +// Campaign demo reset removed โ€” campaign is now at /:space/rsocials/campaign // GET /api/communities/:slug/shapes app.get("/api/communities/:slug/shapes", async (c) => { @@ -834,6 +821,16 @@ app.use("/:space/*", async (c, next) => { // โ”€โ”€ Mount module routes under /:space/:moduleId โ”€โ”€ for (const mod of getAllModules()) { app.route(`/:space/${mod.id}`, mod.routes); + // Auto-mount browsable output list pages + if (mod.outputPaths) { + for (const op of mod.outputPaths) { + app.get(`/:space/${mod.id}/${op.path}`, (c) => { + return c.html(renderOutputListPage( + c.req.param("space"), mod, op, getModuleInfoList() + )); + }); + } + } } // โ”€โ”€ Page routes โ”€โ”€ @@ -1536,7 +1533,6 @@ const server = Bun.serve({ // โ”€โ”€ Startup โ”€โ”€ ensureDemoCommunity().then(() => console.log("[Demo] Demo community ready")).catch((e) => console.error("[Demo] Failed:", e)); -ensureCampaignDemo().then(() => console.log("[Campaign] Campaign demo ready")).catch((e) => console.error("[Campaign] Failed:", e)); loadAllDocs(syncServer).catch((e) => console.error("[DocStore] Startup load failed:", e)); console.log(`rSpace unified server running on http://localhost:${PORT}`); diff --git a/server/output-list.ts b/server/output-list.ts new file mode 100644 index 0000000..17013a7 --- /dev/null +++ b/server/output-list.ts @@ -0,0 +1,143 @@ +/** + * Output list page renderer. + * + * Generates a browsable list page for a module's output type. + * Fetches items from the module's API and renders them as a card grid + * inside the standard rSpace shell. + */ + +import type { ModuleInfo, OutputPath } from "../shared/module"; +import { renderShell, escapeHtml, escapeAttr } from "./shell"; + +export function renderOutputListPage( + space: string, + mod: { id: string; name: string; icon: string; description: string }, + outputPath: OutputPath, + modules: ModuleInfo[], +): string { + const body = ` +
+
+ ${escapeHtml(outputPath.icon)} +
+

${escapeHtml(outputPath.name)}

+

${escapeHtml(outputPath.description)}

+
+
+
+
Loadingโ€ฆ
+
+
+ +`; + + const styles = ``; + + return renderShell({ + title: `${outputPath.name} โ€” ${mod.name} | rSpace`, + moduleId: mod.id, + spaceSlug: space, + body, + modules, + theme: "dark", + styles, + }); +}