feat: add seed template data for rCart, rChoices, rFiles, rForum, rFunds, rInbox, rSplat
Each module now seeds starter content when a new space is created, giving users something to interact with immediately rather than an empty state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9ed9757a2a
commit
1635b08704
|
|
@ -620,6 +620,51 @@ routes.get("/", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateCart(space: string) {
|
||||
if (!_syncServer) return;
|
||||
const doc = ensureCatalogDoc(space);
|
||||
if (Object.keys(doc.items).length > 0) return;
|
||||
|
||||
const docId = catalogDocId(space);
|
||||
const now = Date.now();
|
||||
|
||||
const items: Array<{ title: string; type: string; caps: string[]; subs: string[]; tags: string[] }> = [
|
||||
{
|
||||
title: 'Commons Community Sticker Pack', type: 'sticker',
|
||||
caps: ['laser-print'], subs: ['vinyl-matte'],
|
||||
tags: ['merch', 'stickers'],
|
||||
},
|
||||
{
|
||||
title: 'Cosmolocal Network Poster (A2)', type: 'poster',
|
||||
caps: ['risograph'], subs: ['paper-heavyweight'],
|
||||
tags: ['merch', 'poster', 'cosmolocal'],
|
||||
},
|
||||
{
|
||||
title: 'rSpace Contributor Tee', type: 'apparel',
|
||||
caps: ['screen-print'], subs: ['cotton-organic'],
|
||||
tags: ['merch', 'apparel'],
|
||||
},
|
||||
];
|
||||
|
||||
_syncServer.changeDoc<CatalogDoc>(docId, 'seed template catalog', (d) => {
|
||||
for (const item of items) {
|
||||
const id = crypto.randomUUID();
|
||||
d.items[id] = {
|
||||
id, artifactId: crypto.randomUUID(), artifact: null,
|
||||
title: item.title, productType: item.type,
|
||||
requiredCapabilities: item.caps, substrates: item.subs,
|
||||
creatorId: 'did:demo:seed', sourceSpace: space,
|
||||
tags: item.tags, status: 'active',
|
||||
createdAt: now, updatedAt: now,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[Cart] Template seeded for "${space}": 3 catalog entries`);
|
||||
}
|
||||
|
||||
export const cartModule: RSpaceModule = {
|
||||
id: "rcart",
|
||||
name: "rCart",
|
||||
|
|
@ -633,6 +678,7 @@ export const cartModule: RSpaceModule = {
|
|||
routes,
|
||||
standaloneDomain: "rcart.online",
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateCart,
|
||||
async onInit(ctx) {
|
||||
_syncServer = ctx.syncServer;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { renderShell } from "../../server/shell";
|
|||
import type { RSpaceModule } from "../../shared/module";
|
||||
import { renderLanding } from "./landing";
|
||||
import { getModuleInfoList } from "../../shared/module";
|
||||
import { getDocumentData } from "../../server/community-store";
|
||||
import { getDocumentData, addShapes } from "../../server/community-store";
|
||||
|
||||
const routes = new Hono();
|
||||
|
||||
|
|
@ -61,6 +61,63 @@ routes.get("/", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateChoices(space: string) {
|
||||
// Check if space already has choice shapes
|
||||
const docData = getDocumentData(space);
|
||||
const choiceTypes = ["folk-choice-vote", "folk-choice-rank", "folk-choice-spider"];
|
||||
if (docData?.shapes) {
|
||||
const existing = Object.values(docData.shapes as Record<string, any>)
|
||||
.filter((s: any) => !s.forgotten && choiceTypes.includes(s.type));
|
||||
if (existing.length > 0) return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const shapes: Record<string, unknown>[] = [
|
||||
{
|
||||
id: `tmpl-choice-vote-${now}`, type: 'folk-choice-vote',
|
||||
x: 50, y: 1800, width: 420, height: 360, rotation: 0,
|
||||
title: 'Governance Priority Vote',
|
||||
mode: 'plurality',
|
||||
options: [
|
||||
{ id: crypto.randomUUID(), label: 'Infrastructure improvements', color: '#3b82f6' },
|
||||
{ id: crypto.randomUUID(), label: 'Community education programs', color: '#8b5cf6' },
|
||||
{ id: crypto.randomUUID(), label: 'Open-source tooling grants', color: '#10b981' },
|
||||
],
|
||||
votes: [], createdAt: now,
|
||||
},
|
||||
{
|
||||
id: `tmpl-choice-rank-${now}`, type: 'folk-choice-rank',
|
||||
x: 520, y: 1800, width: 420, height: 360, rotation: 0,
|
||||
title: 'Sprint Priority Ranking',
|
||||
options: [
|
||||
{ id: crypto.randomUUID(), label: 'Dark mode across all modules' },
|
||||
{ id: crypto.randomUUID(), label: 'Mobile-responsive layouts' },
|
||||
{ id: crypto.randomUUID(), label: 'Offline-first sync' },
|
||||
{ id: crypto.randomUUID(), label: 'Notification system' },
|
||||
],
|
||||
rankings: [], createdAt: now,
|
||||
},
|
||||
{
|
||||
id: `tmpl-choice-spider-${now}`, type: 'folk-choice-spider',
|
||||
x: 990, y: 1800, width: 420, height: 360, rotation: 0,
|
||||
title: 'Team Skills Assessment',
|
||||
options: [
|
||||
{ id: crypto.randomUUID(), label: 'Frontend' },
|
||||
{ id: crypto.randomUUID(), label: 'Backend' },
|
||||
{ id: crypto.randomUUID(), label: 'Design' },
|
||||
{ id: crypto.randomUUID(), label: 'DevOps' },
|
||||
{ id: crypto.randomUUID(), label: 'Community' },
|
||||
],
|
||||
scores: [], createdAt: now,
|
||||
},
|
||||
];
|
||||
|
||||
addShapes(space, shapes);
|
||||
console.log(`[Choices] Template seeded for "${space}": 3 choice shapes`);
|
||||
}
|
||||
|
||||
export const choicesModule: RSpaceModule = {
|
||||
id: "rchoices",
|
||||
name: "rChoices",
|
||||
|
|
@ -70,6 +127,7 @@ export const choicesModule: RSpaceModule = {
|
|||
routes,
|
||||
standaloneDomain: "rchoices.online",
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateChoices,
|
||||
feeds: [
|
||||
{
|
||||
id: "poll-results",
|
||||
|
|
|
|||
|
|
@ -630,6 +630,44 @@ routes.get("/", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateFiles(space: string) {
|
||||
if (!_syncServer) return;
|
||||
const doc = ensureDoc(space, 'default');
|
||||
if (Object.keys(doc.files).length > 0 || Object.keys(doc.memoryCards).length > 0) return;
|
||||
|
||||
const docId = filesDocId(space, 'default');
|
||||
const now = Date.now();
|
||||
|
||||
_syncServer.changeDoc<FilesDocExt>(docId, 'seed template files', (d) => {
|
||||
const files: Array<{ name: string; title: string; mime: string; size: number; tags: string[] }> = [
|
||||
{ name: 'project-charter.pdf', title: 'Project Charter', mime: 'application/pdf', size: 245000, tags: ['governance', 'founding'] },
|
||||
{ name: 'logo.svg', title: 'Community Logo', mime: 'image/svg+xml', size: 12400, tags: ['branding', 'design'] },
|
||||
{ name: 'budget-2026.xlsx', title: 'Budget 2026', mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 89000, tags: ['finance', 'planning'] },
|
||||
];
|
||||
for (const f of files) {
|
||||
const id = crypto.randomUUID();
|
||||
d.files[id] = {
|
||||
id, originalFilename: f.name, title: f.title, description: '',
|
||||
mimeType: f.mime, fileSize: f.size, fileHash: null, storagePath: '',
|
||||
tags: f.tags, isProcessed: true, processingError: null,
|
||||
uploadedBy: 'did:demo:seed', sharedSpace: 'default',
|
||||
createdAt: now, updatedAt: now,
|
||||
};
|
||||
}
|
||||
const mcId = crypto.randomUUID();
|
||||
d.memoryCards[mcId] = {
|
||||
id: mcId, sharedSpace: 'default', title: 'Welcome',
|
||||
body: 'This is your shared file space. Upload documents, images, and other files to collaborate with your community.',
|
||||
cardType: 'note', tags: ['onboarding'], position: 0,
|
||||
createdBy: 'did:demo:seed', createdAt: now, updatedAt: now,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`[Files] Template seeded for "${space}": 3 files, 1 memory card`);
|
||||
}
|
||||
|
||||
export const filesModule: RSpaceModule = {
|
||||
id: "rfiles",
|
||||
name: "rFiles",
|
||||
|
|
@ -639,6 +677,7 @@ export const filesModule: RSpaceModule = {
|
|||
docSchemas: [{ pattern: '{space}:files:cards:{sharedSpace}', description: 'Files and memory cards', init: filesSchema.init }],
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateFiles,
|
||||
async onInit(ctx) {
|
||||
_syncServer = ctx.syncServer;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -199,6 +199,31 @@ routes.get("/", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateForum(_space: string) {
|
||||
if (!_syncServer) return;
|
||||
const doc = ensureDoc();
|
||||
if (Object.keys(doc.instances).length > 0) return;
|
||||
|
||||
const now = Date.now();
|
||||
const instanceId = crypto.randomUUID();
|
||||
|
||||
_syncServer.changeDoc<ForumDoc>(FORUM_DOC_ID, 'seed template forum instance', (d) => {
|
||||
d.instances[instanceId] = {
|
||||
id: instanceId, userId: 'did:demo:seed', name: 'Commons Forum',
|
||||
domain: 'commons.rforum.online', status: 'active', errorMessage: '',
|
||||
discourseVersion: '3.2.0', provider: 'hetzner', vpsId: 'demo-vps',
|
||||
vpsIp: '0.0.0.0', region: 'eu-central', size: 'cx21',
|
||||
adminEmail: 'admin@commons.rforum.online', smtpConfig: {},
|
||||
dnsRecordId: '', sslProvisioned: true,
|
||||
createdAt: now, updatedAt: now, provisionedAt: now, destroyedAt: 0,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`[Forum] Template seeded: 1 demo instance`);
|
||||
}
|
||||
|
||||
export const forumModule: RSpaceModule = {
|
||||
id: "rforum",
|
||||
name: "rForum",
|
||||
|
|
@ -208,6 +233,7 @@ export const forumModule: RSpaceModule = {
|
|||
docSchemas: [{ pattern: 'global:forum:instances', description: 'Forum provisioning metadata', init: forumSchema.init }],
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateForum,
|
||||
standaloneDomain: "rforum.online",
|
||||
externalApp: { url: "https://commons.rforum.online", name: "Discourse" },
|
||||
feeds: [
|
||||
|
|
|
|||
|
|
@ -313,6 +313,29 @@ routes.get("/flow/:flowId", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateFunds(space: string) {
|
||||
if (!_syncServer) return;
|
||||
const doc = ensureDoc(space);
|
||||
if (Object.keys(doc.spaceFlows).length > 0) return;
|
||||
|
||||
const docId = fundsDocId(space);
|
||||
const now = Date.now();
|
||||
const flowId = crypto.randomUUID();
|
||||
|
||||
// Create a SpaceFlow entry pointing to "demo" — the frontend
|
||||
// already renders demoNodes from presets.ts in demo mode.
|
||||
_syncServer.changeDoc<FundsDoc>(docId, 'seed template flow', (d) => {
|
||||
d.spaceFlows[flowId] = {
|
||||
id: flowId, spaceSlug: space, flowId: 'demo',
|
||||
addedBy: 'did:demo:seed', createdAt: now,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`[Funds] Template seeded for "${space}": 1 demo flow association`);
|
||||
}
|
||||
|
||||
export const fundsModule: RSpaceModule = {
|
||||
id: "rfunds",
|
||||
name: "rFunds",
|
||||
|
|
@ -322,6 +345,7 @@ export const fundsModule: RSpaceModule = {
|
|||
docSchemas: [{ pattern: '{space}:funds:flows', description: 'Space flow associations', init: fundsSchema.init }],
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateFunds,
|
||||
async onInit(ctx) {
|
||||
_syncServer = ctx.syncServer;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -984,6 +984,72 @@ routes.get("/", (c) => {
|
|||
}));
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateInbox(space: string) {
|
||||
if (!_syncServer) return;
|
||||
// Skip if space already has mailboxes
|
||||
const prefix = `${space}:inbox:mailboxes:`;
|
||||
const existing = _syncServer.listDocs().filter((id) => id.startsWith(prefix));
|
||||
if (existing.length > 0) return;
|
||||
|
||||
const mbId = crypto.randomUUID();
|
||||
const docId = mailboxDocId(space, mbId);
|
||||
const now = Date.now();
|
||||
const day = 86400000;
|
||||
|
||||
const doc = Automerge.change(Automerge.init<MailboxDoc>(), 'seed template mailbox', (d) => {
|
||||
d.meta = { module: 'inbox', collection: 'mailboxes', version: 1, spaceSlug: space, createdAt: now };
|
||||
d.mailbox = {
|
||||
id: mbId, workspaceId: null, slug: 'commons-team', name: 'Commons Team',
|
||||
email: `commons-team@${space}.rspace.online`,
|
||||
description: 'Shared mailbox for the commons coordination team.',
|
||||
visibility: 'members', ownerDid: 'did:demo:seed',
|
||||
safeAddress: null, safeChainId: null, approvalThreshold: 1, createdAt: now,
|
||||
};
|
||||
d.members = [];
|
||||
d.threads = {};
|
||||
d.approvals = {};
|
||||
|
||||
const threads: Array<{ subj: string; from: string; fromName: string; body: string; tags: string[]; age: number }> = [
|
||||
{
|
||||
subj: 'Grant Application Update — Gitcoin GG22',
|
||||
from: 'grants@gitcoin.co', fromName: 'Gitcoin Grants',
|
||||
body: 'Your application for the Cosmolocal Commons project has been accepted into GG22 Climate Solutions round. Matching pool opens April 1.',
|
||||
tags: ['grants', 'important'], age: 2,
|
||||
},
|
||||
{
|
||||
subj: 'Partnership Inquiry — Barcelona Maker Space',
|
||||
from: 'hello@bcnmakers.cat', fromName: 'BCN Makers',
|
||||
body: 'Hi! We saw your cosmolocal print network and would love to join as a provider. We have risograph, screen print, and laser cutting capabilities.',
|
||||
tags: ['partnerships'], age: 5,
|
||||
},
|
||||
{
|
||||
subj: 'Weekly Digest — rSpace Dev Updates',
|
||||
from: 'digest@rspace.online', fromName: 'rSpace Bot',
|
||||
body: '## This Week\n\n- 3 new modules deployed (rChoices, rSplat, rInbox)\n- EncryptID guardian recovery shipped\n- 2 new providers joined the cosmolocal network',
|
||||
tags: ['digest'], age: 1,
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of threads) {
|
||||
const tId = crypto.randomUUID();
|
||||
d.threads[tId] = {
|
||||
id: tId, mailboxId: mbId, messageId: `<${crypto.randomUUID()}@demo>`,
|
||||
subject: t.subj, fromAddress: t.from, fromName: t.fromName,
|
||||
toAddresses: [`commons-team@${space}.rspace.online`], ccAddresses: [],
|
||||
bodyText: t.body, bodyHtml: '', tags: t.tags, status: 'open',
|
||||
isRead: t.age > 3, isStarred: t.tags.includes('important'),
|
||||
assignedTo: null, hasAttachments: false,
|
||||
receivedAt: now - t.age * day, createdAt: now - t.age * day, comments: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
_syncServer.setDoc(docId, doc);
|
||||
console.log(`[Inbox] Template seeded for "${space}": 1 mailbox, 3 threads`);
|
||||
}
|
||||
|
||||
export const inboxModule: RSpaceModule = {
|
||||
id: "rinbox",
|
||||
name: "rInbox",
|
||||
|
|
@ -993,6 +1059,7 @@ export const inboxModule: RSpaceModule = {
|
|||
docSchemas: [{ pattern: '{space}:inbox:mailboxes:{mailboxId}', description: 'Mailbox with threads and approvals', init: mailboxSchema.init }],
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateInbox,
|
||||
async onInit(ctx) {
|
||||
_syncServer = ctx.syncServer;
|
||||
console.log("[Inbox] Module initialized (Automerge-only, no PG)");
|
||||
|
|
|
|||
|
|
@ -669,6 +669,48 @@ routes.get("/view/:id", async (c) => {
|
|||
return c.html(html);
|
||||
});
|
||||
|
||||
// ── Seed template data ──
|
||||
|
||||
function seedTemplateSplat(space: string) {
|
||||
if (!_syncServer) return;
|
||||
const doc = ensureDoc(space);
|
||||
if (Object.keys(doc.items).length > 0) return;
|
||||
|
||||
const docId = splatScenesDocId(space);
|
||||
const now = Date.now();
|
||||
|
||||
const scenes: Array<{ title: string; slug: string; desc: string; tags: string[] }> = [
|
||||
{
|
||||
title: 'Community Garden Scan', slug: 'community-garden-scan',
|
||||
desc: 'A 3D Gaussian splat capture of the community garden space, captured with a phone camera walk-around.',
|
||||
tags: ['outdoor', 'community', 'garden'],
|
||||
},
|
||||
{
|
||||
title: 'Workshop Space Scan', slug: 'workshop-space-scan',
|
||||
desc: 'Interior scan of the maker workshop, showing CNC, 3D printer stations, and material shelving.',
|
||||
tags: ['indoor', 'workshop', 'makerspace'],
|
||||
},
|
||||
];
|
||||
|
||||
_syncServer.changeDoc<SplatScenesDoc>(docId, 'seed template splats', (d) => {
|
||||
for (const s of scenes) {
|
||||
const id = crypto.randomUUID();
|
||||
d.items[id] = {
|
||||
id, slug: s.slug, title: s.title, description: s.desc,
|
||||
filePath: '', fileFormat: 'splat', fileSizeBytes: 0,
|
||||
tags: s.tags, spaceSlug: space,
|
||||
contributorId: 'did:demo:seed', contributorName: 'Demo',
|
||||
source: 'upload', status: 'published', viewCount: 0,
|
||||
paymentTx: null, paymentNetwork: null,
|
||||
createdAt: now, processingStatus: 'ready', processingError: null,
|
||||
sourceFileCount: 0, sourceFiles: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[Splat] Template seeded for "${space}": 2 splat entries`);
|
||||
}
|
||||
|
||||
// ── Module export ──
|
||||
|
||||
export const splatModule: RSpaceModule = {
|
||||
|
|
@ -680,6 +722,7 @@ export const splatModule: RSpaceModule = {
|
|||
docSchemas: [{ pattern: '{space}:splat:scenes', description: 'Splat scene metadata', init: splatScenesSchema.init }],
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
seedTemplate: seedTemplateSplat,
|
||||
standaloneDomain: "rsplat.online",
|
||||
hidden: true,
|
||||
outputPaths: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue