feat(rsocials): hub navigation page, route renames, and campaigns listing

Restructure rSocials landing to a hub with navigation cards for Campaigns,
Threads, and Thread Editor. Rename /thread routes to /thread-editor for
clarity. Render campaigns listing inline instead of redirecting. Also
improve rNotes notebook creation to auto-open first note for editing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 13:31:23 -07:00
parent b8f32a863e
commit 6173e17bb9
6 changed files with 93 additions and 34 deletions

View File

@ -360,17 +360,31 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
}
private demoCreateNotebook() {
const title = prompt("Notebook name:");
if (!title?.trim()) return;
const now = Date.now();
const nbId = `demo-nb-${now}`;
const noteId = `demo-note-${now}`;
const newNote: Note = {
id: noteId, title: "Untitled Note", content: "", content_plain: "",
content_format: 'tiptap-json',
type: "NOTE", tags: null, is_pinned: false,
created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
};
const nb = {
id: `demo-nb-${now}`, title, description: "",
cover_color: "#8b5cf6", note_count: "0",
updated_at: new Date(now).toISOString(), notes: [] as Note[],
id: nbId, title: "Untitled Notebook", description: "",
cover_color: "#8b5cf6", note_count: "1",
updated_at: new Date(now).toISOString(), notes: [newNote],
} as any;
this.demoNotebooks.push(nb);
this.notebooks = this.demoNotebooks.map(({ notes, ...rest }) => rest as Notebook);
this.selectedNotebook = { ...nb };
this.view = "notebook";
this.render();
// Auto-open the note for editing
this.selectedNote = newNote;
this.view = "note";
this.renderNav();
this.renderMeta();
this.mountEditor(newNote);
}
private demoCreateNote() {

View File

@ -131,7 +131,7 @@ export class FolkCampaignManager extends HTMLElement {
</div>
</div>
<div class="actions">
<a href="/${this.esc(this._space)}/rsocials/thread" class="btn btn--outline">Open Thread Builder</a>
<a href="/${this.esc(this._space)}/rsocials/thread-editor" class="btn btn--outline">Open Thread Builder</a>
<button class="btn btn--primary" id="import-md-btn">Import from Markdown</button>
</div>
${phaseHTML}

View File

@ -820,7 +820,7 @@ class FolkCampaignPlanner extends HTMLElement {
} else if (action === 'open-thread') {
const d = node.data as ThreadNodeData;
if (d.threadId) {
window.location.href = `/${this.space}/rsocials/thread/${d.threadId}/edit`;
window.location.href = `/${this.space}/rsocials/thread-editor/${d.threadId}/edit`;
}
}
});
@ -1457,7 +1457,7 @@ class FolkCampaignPlanner extends HTMLElement {
if (node?.type === 'thread') {
const d = node.data as ThreadNodeData;
if (d.threadId) {
window.location.href = `/${this.space}/rsocials/thread/${d.threadId}/edit`;
window.location.href = `/${this.space}/rsocials/thread-editor/${d.threadId}/edit`;
}
}
});

View File

@ -274,7 +274,7 @@ export class FolkThreadBuilder extends HTMLElement {
</div>
<div class="preview ro-cards">${tweetCards}</div>
<div class="ro-actions">
<a href="/${this.esc(this._space)}/rsocials/thread/${this.esc(t.id)}/edit" class="btn btn--primary">Edit Thread</a>
<a href="/${this.esc(this._space)}/rsocials/thread-editor/${this.esc(t.id)}/edit" class="btn btn--primary">Edit Thread</a>
<button class="btn btn--outline" id="ro-copy-thread">Copy Thread</button>
<button class="btn btn--outline" id="ro-copy-link">Copy Link</button>
<div class="export-dropdown">
@ -289,7 +289,7 @@ export class FolkThreadBuilder extends HTMLElement {
</div>
</div>
<div class="ro-cta">
<a href="/${this.esc(this._space)}/rsocials/thread" class="btn btn--success">Create Your Own Thread</a>
<a href="/${this.esc(this._space)}/rsocials/thread-editor" class="btn btn--success">Create Your Own Thread</a>
<a href="/${this.esc(this._space)}/rsocials/threads" class="btn btn--outline">Browse All Threads</a>
</div>
</div>

View File

@ -96,7 +96,7 @@ export class FolkThreadGallery extends HTMLElement {
const cardsHTML = threads.length === 0
? `<div class="empty">
<p>No threads yet. Create your first thread!</p>
<a href="/${this.esc(space)}/rsocials/thread" class="btn btn--success">Create Thread</a>
<a href="/${this.esc(space)}/rsocials/thread-editor" class="btn btn--success">Create Thread</a>
</div>`
: `<div class="grid">
${threads.map(t => {
@ -164,7 +164,7 @@ export class FolkThreadGallery extends HTMLElement {
<div class="gallery">
<div class="header">
<h1>Threads</h1>
<a href="/${this.esc(space)}/rsocials/thread" class="btn btn--primary">New Thread</a>
<a href="/${this.esc(space)}/rsocials/thread-editor" class="btn btn--primary">New Thread</a>
</div>
${cardsHTML}
</div>

View File

@ -459,7 +459,7 @@ routes.get("/thread/:id", async (c) => {
}));
});
routes.get("/thread/:id/edit", async (c) => {
routes.get("/thread-editor/:id/edit", async (c) => {
const space = c.req.param("space") || "demo";
const id = c.req.param("id");
if (!id || id.includes("..") || id.includes("/")) return c.text("Not found", 404);
@ -481,10 +481,10 @@ routes.get("/thread/:id/edit", async (c) => {
}));
});
routes.get("/thread", (c) => {
routes.get("/thread-editor", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `Thread Builder — rSocials | rSpace`,
title: `Thread Editor — rSocials | rSpace`,
moduleId: "rsocials",
spaceSlug: space,
modules: getModuleInfoList(),
@ -511,7 +511,16 @@ routes.get("/threads", (c) => {
routes.get("/campaigns", (c) => {
const space = c.req.param("space") || "demo";
return c.redirect(`/${space}/rsocials/campaign`);
return c.html(renderShell({
title: `Campaigns — rSocials | rSpace`,
moduleId: "rsocials",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
body: `<folk-campaign-manager space="${escapeHtml(space)}"></folk-campaign-manager>`,
styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
scripts: `<script type="module" src="/modules/rsocials/folk-campaign-manager.js"></script>`,
}));
});
// ── Demo feed rendering (server-rendered, no web component needed) ──
@ -600,19 +609,55 @@ routes.get("/landing", (c) => {
}));
});
// ── Default: campaign planner canvas ──
// ── Default: rSocials hub with navigation ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
const base = `/${escapeHtml(space)}/rsocials`;
return c.html(renderShell({
title: `${space} — rSocials | rSpace`,
title: `rSocials — ${space} | rSpace`,
moduleId: "rsocials",
spaceSlug: space,
modules: getModuleInfoList(),
body: `<folk-campaign-planner space="${escapeHtml(space)}"></folk-campaign-planner>`,
scripts: `<script type="module" src="/modules/rsocials/folk-campaign-planner.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/rsocials/campaign-planner.css">`,
theme: "dark",
styles: `<style>
.rs-hub{max-width:720px;margin:3rem auto;padding:0 1.5rem}
.rs-hub h1{font-size:1.8rem;margin-bottom:.5rem}
.rs-hub p{color:var(--rs-text-secondary,#aaa);margin-bottom:2rem}
.rs-nav{display:flex;flex-direction:column;gap:1rem}
.rs-nav a{display:flex;align-items:center;gap:1rem;padding:1.25rem 1.5rem;border-radius:12px;background:var(--rs-surface,#1e1e2e);border:1px solid var(--rs-border,#333);text-decoration:none;color:inherit;transition:border-color .15s,background .15s}
.rs-nav a:hover{border-color:var(--rs-accent,#14b8a6);background:var(--rs-surface-hover,#252538)}
.rs-nav .nav-icon{font-size:2rem;flex-shrink:0}
.rs-nav .nav-body h3{margin:0 0 .25rem;font-size:1.1rem}
.rs-nav .nav-body p{margin:0;font-size:.85rem;color:var(--rs-text-secondary,#aaa)}
</style>`,
body: `<div class="rs-hub">
<h1>rSocials</h1>
<p>Social media tools for your community</p>
<nav class="rs-nav">
<a href="${base}/campaigns">
<span class="nav-icon">📢</span>
<div class="nav-body">
<h3>Campaigns</h3>
<p>Plan and manage multi-platform social media campaigns</p>
</div>
</a>
<a href="${base}/threads">
<span class="nav-icon">📋</span>
<div class="nav-body">
<h3>Threads</h3>
<p>Browse saved thread drafts and published threads</p>
</div>
</a>
<a href="${base}/thread-editor">
<span class="nav-icon">🧵</span>
<div class="nav-body">
<h3>Thread Editor</h3>
<p>Compose and preview tweet threads with live card preview</p>
</div>
</a>
</nav>
</div>`,
}));
});
@ -651,19 +696,7 @@ export const socialsModule: RSpaceModule = {
],
subPageInfos: [
{
path: "thread",
title: "Thread Builder",
icon: "🧵",
tagline: "rSocials Tool",
description: "Compose, preview, and schedule tweet threads with a live card-by-card preview. Save drafts, generate share images, and publish when ready.",
features: [
{ icon: "✍️", title: "Live Preview", text: "See your thread as tweet cards in real time as you type, with character counts and thread numbering." },
{ icon: "💾", title: "Save & Edit Drafts", text: "Save thread drafts to your space, revisit and refine them before publishing." },
{ icon: "🖼️", title: "Share Images", text: "Auto-generate a branded share image of your thread for cross-posting." },
],
},
{
path: "campaign",
path: "campaigns",
title: "Campaign Manager",
icon: "📢",
tagline: "rSocials Tool",
@ -681,5 +714,17 @@ export const socialsModule: RSpaceModule = {
tagline: "rSocials Tool",
description: "Browse all saved thread drafts in your community. Find inspiration, remix threads, or pick up where you left off.",
},
{
path: "thread-editor",
title: "Thread Editor",
icon: "🧵",
tagline: "rSocials Tool",
description: "Compose, preview, and schedule tweet threads with a live card-by-card preview. Save drafts, generate share images, and publish when ready.",
features: [
{ icon: "✍️", title: "Live Preview", text: "See your thread as tweet cards in real time as you type, with character counts and thread numbering." },
{ icon: "💾", title: "Save & Edit Drafts", text: "Save thread drafts to your space, revisit and refine them before publishing." },
{ icon: "🖼️", title: "Share Images", text: "Auto-generate a branded share image of your thread for cross-posting." },
],
},
],
};