Merge branch 'dev'
This commit is contained in:
commit
96b0a48436
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from "./community-store";
|
||||
import type { NestPermissions, SpaceRefFilter } from "./community-store";
|
||||
import { ensureDemoCommunity } from "./seed-demo";
|
||||
import { seedTemplateShapes, ensureTemplateSeeding } from "./seed-template";
|
||||
// Campaign demo moved to rsocials module — see modules/rsocials/campaign-data.ts
|
||||
import type { SpaceVisibility } from "./community-store";
|
||||
import {
|
||||
|
|
@ -817,6 +818,27 @@ app.use("/:space/*", async (c, next) => {
|
|||
);
|
||||
});
|
||||
|
||||
// ── Template seeding route: /:space/:moduleId/template ──
|
||||
// Triggers template seeding for empty spaces, then redirects to the module page.
|
||||
app.get("/:space/:moduleId/template", async (c) => {
|
||||
const space = c.req.param("space");
|
||||
const moduleId = c.req.param("moduleId");
|
||||
if (!space || space === "api" || space.includes(".")) return c.notFound();
|
||||
|
||||
try {
|
||||
await loadCommunity(space);
|
||||
const seeded = seedTemplateShapes(space);
|
||||
if (seeded) {
|
||||
broadcastAutomergeSync(space);
|
||||
broadcastJsonSnapshot(space);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Template] On-demand seed failed for "${space}":`, e);
|
||||
}
|
||||
|
||||
return c.redirect(`/${space}/${moduleId}`, 302);
|
||||
});
|
||||
|
||||
// ── Mount module routes under /:space/:moduleId ──
|
||||
// Enforce enabledModules: if a space has an explicit list, only those modules route.
|
||||
// The 'rspace' (canvas) module is always allowed as the core module.
|
||||
|
|
@ -1348,6 +1370,22 @@ const server = Bun.serve<WSData>({
|
|||
return app.fetch(req);
|
||||
}
|
||||
|
||||
// Template seeding: /{moduleId}/template → seed + redirect
|
||||
if (pathSegments.length >= 2 && pathSegments[pathSegments.length - 1] === "template") {
|
||||
try {
|
||||
await loadCommunity(subdomain);
|
||||
const seeded = seedTemplateShapes(subdomain);
|
||||
if (seeded) {
|
||||
broadcastAutomergeSync(subdomain);
|
||||
broadcastJsonSnapshot(subdomain);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Template] On-demand seed failed for "${subdomain}":`, e);
|
||||
}
|
||||
const withoutTemplate = pathSegments.slice(0, -1).join("/");
|
||||
return Response.redirect(`${url.protocol}//${url.host}/${withoutTemplate}`, 302);
|
||||
}
|
||||
|
||||
// Normalize module ID to lowercase (rTrips → rtrips)
|
||||
const normalizedPath = "/" + pathSegments.map((seg, i) =>
|
||||
i === 0 ? seg.toLowerCase() : seg
|
||||
|
|
@ -1698,7 +1736,9 @@ const server = Bun.serve<WSData>({
|
|||
})();
|
||||
|
||||
ensureDemoCommunity().then(() => console.log("[Demo] Demo community ready")).catch((e) => console.error("[Demo] Failed:", e));
|
||||
loadAllDocs(syncServer).catch((e) => console.error("[DocStore] Startup load failed:", e));
|
||||
loadAllDocs(syncServer)
|
||||
.then(() => ensureTemplateSeeding())
|
||||
.catch((e) => console.error("[DocStore] Startup load failed:", e));
|
||||
|
||||
// Restore relay mode for encrypted spaces
|
||||
(async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,454 @@
|
|||
/**
|
||||
* Template seeder for new rSpace communities.
|
||||
*
|
||||
* Every non-demo space gets generic "Getting Started" content covering
|
||||
* all 25+ modules so users immediately see what each rApp can do.
|
||||
*
|
||||
* Shape IDs use a `tmpl-` prefix so they never collide with `demo-` or
|
||||
* user-created shapes. Seeding is idempotent — only spaces with 0 shapes
|
||||
* are touched, and the demo space is always skipped.
|
||||
*/
|
||||
|
||||
import {
|
||||
addShapes,
|
||||
getDocumentData,
|
||||
listCommunities,
|
||||
loadCommunity,
|
||||
} from "./community-store";
|
||||
|
||||
// ── Template Shapes ─────────────────────────────────────────────────
|
||||
|
||||
const TEMPLATE_SHAPES: Record<string, unknown>[] = [
|
||||
// ─── rTrips: Itinerary ──────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-itinerary",
|
||||
type: "folk-itinerary",
|
||||
x: 50, y: 50, width: 400, height: 300, rotation: 0,
|
||||
tripTitle: "My Trip",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
travelers: [],
|
||||
items: [
|
||||
{ date: "Day 1", activity: "Arrive & settle in", category: "travel" },
|
||||
{ date: "Day 2", activity: "Explore the area", category: "adventure" },
|
||||
{ date: "Day 3", activity: "Head home", category: "travel" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── rTrips: Destination ────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-destination",
|
||||
type: "folk-destination",
|
||||
x: 500, y: 50, width: 300, height: 200, rotation: 0,
|
||||
destName: "My Destination",
|
||||
country: "",
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
arrivalDate: "",
|
||||
departureDate: "",
|
||||
notes: "Add your destination details here.",
|
||||
},
|
||||
|
||||
// ─── rNotes: Notebook ───────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-notebook",
|
||||
type: "folk-notebook",
|
||||
x: 850, y: 50, width: 300, height: 180, rotation: 0,
|
||||
notebookTitle: "My Notebook",
|
||||
description: "A shared notebook for your space. Add notes, ideas, and reference material.",
|
||||
noteCount: 0,
|
||||
collaborators: [],
|
||||
},
|
||||
|
||||
// ─── rNotes: Welcome Note ───────────────────────────────────
|
||||
{
|
||||
id: "tmpl-note-welcome",
|
||||
type: "folk-note",
|
||||
x: 850, y: 260, width: 300, height: 250, rotation: 0,
|
||||
noteTitle: "Welcome to rSpace",
|
||||
content: "## Getting Started\n\nThis is your space! Here's what you can do:\n\n- **Explore** the canvas — drag, zoom, and click shapes\n- **Create** new items using the module sidebar\n- **Collaborate** in real-time with your team\n- **Customize** by rearranging or removing these starter shapes\n\nEach shape represents an rApp module. Click one to dive in!",
|
||||
tags: ["getting-started"],
|
||||
editor: "",
|
||||
editedAt: "",
|
||||
},
|
||||
|
||||
// ─── rNotes: Ideas Note ─────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-note-ideas",
|
||||
type: "folk-note",
|
||||
x: 1200, y: 50, width: 300, height: 200, rotation: 0,
|
||||
noteTitle: "Ideas & Brainstorms",
|
||||
content: "## Ideas\n\nCapture your ideas here.\n\n- Idea 1\n- Idea 2\n- Idea 3\n\nUse tags to organize your notes across the space.",
|
||||
tags: ["ideas"],
|
||||
editor: "",
|
||||
editedAt: "",
|
||||
},
|
||||
|
||||
// ─── rVote: Poll ────────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-poll",
|
||||
type: "demo-poll",
|
||||
x: 50, y: 400, width: 350, height: 200, rotation: 0,
|
||||
question: "What should we work on first?",
|
||||
options: [
|
||||
{ label: "Set up the space", votes: 0 },
|
||||
{ label: "Invite team members", votes: 0 },
|
||||
{ label: "Plan our first project", votes: 0 },
|
||||
],
|
||||
totalVoters: 0,
|
||||
status: "active",
|
||||
endsAt: "",
|
||||
},
|
||||
|
||||
// ─── rCart: Item ────────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-cart-item",
|
||||
type: "demo-cart-item",
|
||||
x: 50, y: 630, width: 320, height: 60, rotation: 0,
|
||||
name: "Example Item",
|
||||
price: 0,
|
||||
funded: 0,
|
||||
status: "In Cart",
|
||||
requestedBy: "",
|
||||
store: "",
|
||||
},
|
||||
|
||||
// ─── rFunds: Budget ─────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-budget",
|
||||
type: "folk-budget",
|
||||
x: 450, y: 400, width: 350, height: 250, rotation: 0,
|
||||
budgetTitle: "Space Budget",
|
||||
currency: "USD",
|
||||
budgetTotal: 0,
|
||||
spent: 0,
|
||||
categories: [
|
||||
{ name: "Operations", budget: 0, spent: 0 },
|
||||
{ name: "Marketing", budget: 0, spent: 0 },
|
||||
{ name: "Development", budget: 0, spent: 0 },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── rFunds: Expense ────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-expense",
|
||||
type: "demo-expense",
|
||||
x: 450, y: 680, width: 320, height: 60, rotation: 0,
|
||||
description: "Example expense",
|
||||
amount: 0,
|
||||
currency: "USD",
|
||||
paidBy: "",
|
||||
split: "equal",
|
||||
category: "operations",
|
||||
date: "",
|
||||
},
|
||||
|
||||
// ─── rMaps: Location Marker ─────────────────────────────────
|
||||
{
|
||||
id: "tmpl-map-marker",
|
||||
type: "demo-map-marker",
|
||||
x: 500, y: 280, width: 40, height: 40, rotation: 0,
|
||||
name: "Home Base",
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
emoji: "📍",
|
||||
category: "home",
|
||||
status: "",
|
||||
},
|
||||
|
||||
// ─── rTokens: Mint ──────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-mint",
|
||||
type: "folk-token-mint",
|
||||
x: 1550, y: 50, width: 320, height: 280, rotation: 0,
|
||||
tokenName: "Contribution Token",
|
||||
tokenSymbol: "CONTRIB",
|
||||
description: "Track and reward contributions to your space. Customize the name, symbol, and supply.",
|
||||
totalSupply: 1000,
|
||||
issuedSupply: 0,
|
||||
tokenColor: "#6d28d9",
|
||||
tokenIcon: "⭐",
|
||||
createdBy: "",
|
||||
createdAt: "",
|
||||
},
|
||||
|
||||
// ─── rTokens: Ledger ────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-ledger",
|
||||
type: "folk-token-ledger",
|
||||
x: 1940, y: 50, width: 380, height: 400, rotation: 0,
|
||||
mintId: "tmpl-mint",
|
||||
entries: [],
|
||||
},
|
||||
|
||||
// ─── rTokens: Arrow ─────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-arrow-tokens",
|
||||
type: "folk-arrow",
|
||||
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
||||
sourceId: "tmpl-mint",
|
||||
targetId: "tmpl-ledger",
|
||||
color: "#6d28d9",
|
||||
},
|
||||
|
||||
// ─── rFiles: File ───────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-file-readme",
|
||||
type: "folk-file",
|
||||
x: 1550, y: 500, width: 280, height: 80, rotation: 0,
|
||||
fileName: "README.md",
|
||||
fileSize: "0 KB",
|
||||
mimeType: "text/markdown",
|
||||
uploadedBy: "",
|
||||
uploadedAt: "",
|
||||
tags: ["getting-started"],
|
||||
},
|
||||
|
||||
// ─── rForum: Thread ─────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-forum-intro",
|
||||
type: "folk-forum-thread",
|
||||
x: 1550, y: 620, width: 320, height: 160, rotation: 0,
|
||||
threadTitle: "Introductions",
|
||||
author: "",
|
||||
createdAt: "",
|
||||
replyCount: 0,
|
||||
lastReply: "",
|
||||
preview: "Introduce yourself to the space! Share who you are and what you're excited to work on.",
|
||||
tags: ["introductions"],
|
||||
},
|
||||
|
||||
// ─── rBooks: Book ───────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-book",
|
||||
type: "folk-book",
|
||||
x: 1940, y: 500, width: 280, height: 200, rotation: 0,
|
||||
bookTitle: "Getting Started Guide",
|
||||
author: "rSpace",
|
||||
coverColor: "#3b82f6",
|
||||
pageCount: 0,
|
||||
currentPage: 0,
|
||||
readers: [],
|
||||
status: "want-to-read",
|
||||
},
|
||||
|
||||
// ─── rPubs: Publication ─────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-pub",
|
||||
type: "folk-pub",
|
||||
x: 50, y: 780, width: 300, height: 180, rotation: 0,
|
||||
pubTitle: "My First Publication",
|
||||
pubType: "zine",
|
||||
creator: "",
|
||||
format: "",
|
||||
status: "draft",
|
||||
copies: 0,
|
||||
price: 0,
|
||||
currency: "USD",
|
||||
description: "A placeholder publication. Edit this to create your first zine, booklet, or print artifact.",
|
||||
},
|
||||
|
||||
// ─── rSwag: Item ────────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-swag",
|
||||
type: "folk-swag",
|
||||
x: 400, y: 780, width: 280, height: 120, rotation: 0,
|
||||
swagTitle: "Space Sticker",
|
||||
swagType: "sticker",
|
||||
designer: "",
|
||||
sizes: [],
|
||||
price: 0,
|
||||
currency: "USD",
|
||||
status: "draft",
|
||||
orderCount: 0,
|
||||
},
|
||||
|
||||
// ─── rProviders: Provider ───────────────────────────────────
|
||||
{
|
||||
id: "tmpl-provider",
|
||||
type: "folk-provider",
|
||||
x: 400, y: 930, width: 300, height: 160, rotation: 0,
|
||||
providerName: "Local Print Shop",
|
||||
location: "",
|
||||
capabilities: ["print", "stickers"],
|
||||
substrates: ["paper", "vinyl"],
|
||||
turnaround: "",
|
||||
rating: 0,
|
||||
ordersFulfilled: 0,
|
||||
},
|
||||
|
||||
// ─── rWork: Task Board ──────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-work-board",
|
||||
type: "folk-work-board",
|
||||
x: 750, y: 780, width: 500, height: 280, rotation: 0,
|
||||
boardTitle: "Getting Started Tasks",
|
||||
columns: [
|
||||
{
|
||||
name: "To Do",
|
||||
tasks: [
|
||||
{ title: "Customize your space", assignee: "", priority: "high" },
|
||||
{ title: "Invite collaborators", assignee: "", priority: "medium" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "In Progress",
|
||||
tasks: [
|
||||
{ title: "Explore the modules", assignee: "", priority: "medium" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Done",
|
||||
tasks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ─── rCal: Calendar ─────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-calendar",
|
||||
type: "folk-calendar",
|
||||
x: 50, y: 1000, width: 350, height: 250, rotation: 0,
|
||||
calTitle: "My Calendar",
|
||||
month: "",
|
||||
events: [
|
||||
{ date: "Today", title: "Space Created", color: "#22c55e" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── rNetwork: Graph ────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-network",
|
||||
type: "folk-network",
|
||||
x: 1300, y: 780, width: 400, height: 280, rotation: 0,
|
||||
networkTitle: "My Network",
|
||||
nodes: [
|
||||
{ id: "me", label: "Me", role: "organizer" },
|
||||
],
|
||||
edges: [],
|
||||
},
|
||||
|
||||
// ─── rTube: Video ───────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-video",
|
||||
type: "folk-video",
|
||||
x: 1750, y: 780, width: 300, height: 180, rotation: 0,
|
||||
videoTitle: "Welcome Video",
|
||||
duration: "",
|
||||
creator: "",
|
||||
uploadedAt: "",
|
||||
views: 0,
|
||||
thumbnail: "",
|
||||
},
|
||||
|
||||
// ─── rInbox: Inbox ──────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-inbox",
|
||||
type: "folk-inbox",
|
||||
x: 1750, y: 990, width: 300, height: 160, rotation: 0,
|
||||
inboxTitle: "Inbox",
|
||||
messages: [
|
||||
{ from: "rSpace", text: "Welcome to your new space! Start by exploring the canvas.", time: "" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── rData: Dashboard ───────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-dashboard",
|
||||
type: "folk-dashboard",
|
||||
x: 50, y: 1280, width: 320, height: 220, rotation: 0,
|
||||
dashTitle: "Space Dashboard",
|
||||
metrics: [
|
||||
{ label: "Members", value: "1", trend: "neutral" },
|
||||
{ label: "Shapes", value: "0", trend: "neutral" },
|
||||
{ label: "Tasks", value: "0/3", trend: "neutral" },
|
||||
],
|
||||
},
|
||||
|
||||
// ─── rChoices: Decision Matrix ──────────────────────────────
|
||||
{
|
||||
id: "tmpl-choices",
|
||||
type: "folk-choice-matrix",
|
||||
x: 420, y: 1280, width: 320, height: 200, rotation: 0,
|
||||
choiceTitle: "Decision Matrix",
|
||||
options: [
|
||||
{ name: "Option A", score: 0, criteria: {} },
|
||||
{ name: "Option B", score: 0, criteria: {} },
|
||||
],
|
||||
decidedBy: "",
|
||||
status: "open",
|
||||
winner: "",
|
||||
},
|
||||
|
||||
// ─── rSplat: 3D Capture ─────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-splat",
|
||||
type: "folk-splat",
|
||||
x: 790, y: 1280, width: 300, height: 160, rotation: 0,
|
||||
splatTitle: "My 3D Capture",
|
||||
pointCount: "0",
|
||||
capturedBy: "",
|
||||
capturedAt: "",
|
||||
fileSize: "0 KB",
|
||||
status: "pending",
|
||||
},
|
||||
|
||||
// ─── rNotes: Packing List ───────────────────────────────────
|
||||
{
|
||||
id: "tmpl-packing",
|
||||
type: "folk-packing-list",
|
||||
x: 1140, y: 1280, width: 300, height: 300, rotation: 0,
|
||||
listTitle: "My Packing List",
|
||||
items: [
|
||||
{ name: "Item 1", packed: false, category: "general" },
|
||||
{ name: "Item 2", packed: false, category: "general" },
|
||||
{ name: "Item 3", packed: false, category: "general" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Seed template shapes into a space if it has 0 shapes.
|
||||
* Skips the demo space. Returns true if shapes were added.
|
||||
*/
|
||||
export function seedTemplateShapes(slug: string): boolean {
|
||||
if (slug === "demo") return false;
|
||||
|
||||
const data = getDocumentData(slug);
|
||||
const shapeCount = data ? Object.keys(data.shapes || {}).length : 0;
|
||||
|
||||
if (shapeCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
addShapes(slug, TEMPLATE_SHAPES);
|
||||
console.log(`[Template] Seeded ${TEMPLATE_SHAPES.length} template shapes into "${slug}"`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate all existing communities on startup and seed any
|
||||
* empty (0-shape) spaces with template content.
|
||||
*/
|
||||
export async function ensureTemplateSeeding(): Promise<void> {
|
||||
const slugs = await listCommunities();
|
||||
let seededCount = 0;
|
||||
|
||||
for (const slug of slugs) {
|
||||
if (slug === "demo") continue;
|
||||
try {
|
||||
await loadCommunity(slug);
|
||||
if (seedTemplateShapes(slug)) {
|
||||
seededCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Template] Failed to seed "${slug}":`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (seededCount > 0) {
|
||||
console.log(`[Template] Retroactively seeded ${seededCount} space(s)`);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ import type { EncryptIDClaims } from "@encryptid/sdk/server";
|
|||
import { getAllModules, getModule } from "../shared/module";
|
||||
import type { SpaceLifecycleContext } from "../shared/module";
|
||||
import { syncServer } from "./sync-instance";
|
||||
import { seedTemplateShapes } from "./seed-template";
|
||||
|
||||
// ── Unified space creation ──
|
||||
|
||||
|
|
@ -98,6 +99,13 @@ export async function createSpace(opts: CreateSpaceOpts): Promise<CreateSpaceRes
|
|||
}
|
||||
}
|
||||
|
||||
// Seed generic template content (non-fatal)
|
||||
try {
|
||||
seedTemplateShapes(slug);
|
||||
} catch (e) {
|
||||
console.error(`[createSpace:${source}] Template seeding failed for ${slug}:`, e);
|
||||
}
|
||||
|
||||
console.log(`[createSpace:${source}] Created space: ${slug}`);
|
||||
return { ok: true, slug, name, visibility, ownerDID };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue