rspace-online/modules/crowdsurf/mod.ts

152 lines
4.7 KiB
TypeScript

/**
* CrowdSurf module — swipe-based community activity coordination.
*
* Inspired by gospelofchange/Crowdsurfing. Users propose activities with
* commitment thresholds; others swipe to join and declare contributions.
* When enough people commit, the activity triggers.
*
* The folk-crowdsurf-dashboard web component lives in components/.
* This module provides:
* - A dashboard listing active/triggered prompts in the current space
* - API to query crowdsurf prompts from the Automerge store
* - Canvas shape integration for inline prompt cards
*/
import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import { getModuleInfoList } from "../../shared/module";
import { getDocumentData, addShapes } from "../../server/community-store";
const routes = new Hono();
// GET /api/crowdsurf — list crowdsurf prompt shapes in the current space
routes.get("/api/crowdsurf", async (c) => {
const space = c.req.param("space") || c.req.query("space") || "demo";
const docData = getDocumentData(space);
if (!docData?.shapes) {
return c.json({ prompts: [], total: 0 });
}
const promptTypes = ["folk-crowdsurf-prompt"];
const prompts: any[] = [];
for (const [id, shape] of Object.entries(docData.shapes as Record<string, any>)) {
if (shape.forgotten) continue;
if (promptTypes.includes(shape.type)) {
prompts.push({
id,
type: shape.type,
text: shape.text || "Untitled",
location: shape.location || "",
threshold: shape.threshold || 3,
swipeCount: Object.keys(shape.swipes || {}).length,
triggered: shape.triggered || false,
createdAt: shape.createdAt,
});
}
}
return c.json({ prompts, total: prompts.length });
});
// GET / — crowdsurf dashboard page
routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${spaceSlug} — CrowdSurf | rSpace`,
moduleId: "crowdsurf",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
body: `<folk-crowdsurf-dashboard space="${spaceSlug}"></folk-crowdsurf-dashboard>`,
scripts: `<script type="module" src="/modules/crowdsurf/folk-crowdsurf-dashboard.js?v=1"></script>`,
styles: `<link rel="stylesheet" href="/modules/crowdsurf/crowdsurf.css">`,
}));
});
// ── Seed template data ──
function seedTemplateCrowdSurf(space: string) {
const docData = getDocumentData(space);
const promptTypes = ["folk-crowdsurf-prompt"];
if (docData?.shapes) {
const existing = Object.values(docData.shapes as Record<string, any>)
.filter((s: any) => !s.forgotten && promptTypes.includes(s.type));
if (existing.length > 0) return;
}
const now = Date.now();
const shapes: Record<string, unknown>[] = [
{
id: `tmpl-crowdsurf-1-${now}`, type: 'folk-crowdsurf-prompt',
x: 50, y: 2200, width: 420, height: 300, rotation: 0,
text: 'Community Garden Planting Day',
location: 'Community Center Garden',
threshold: 5,
duration: 4,
activityDuration: '3 hours',
swipes: {},
triggered: false,
expired: false,
createdAt: now,
},
{
id: `tmpl-crowdsurf-2-${now}`, type: 'folk-crowdsurf-prompt',
x: 520, y: 2200, width: 420, height: 300, rotation: 0,
text: 'Open Mic & Jam Session',
location: 'Local Park Bandstand',
threshold: 8,
duration: 6,
activityDuration: '2 hours',
swipes: {},
triggered: false,
expired: false,
createdAt: now,
},
{
id: `tmpl-crowdsurf-3-${now}`, type: 'folk-crowdsurf-prompt',
x: 990, y: 2200, width: 420, height: 300, rotation: 0,
text: 'Repair Cafe — Bring Your Broken Stuff',
location: 'Maker Space',
threshold: 3,
duration: 8,
activityDuration: '4 hours',
swipes: {},
triggered: false,
expired: false,
createdAt: now,
},
];
addShapes(space, shapes);
console.log(`[CrowdSurf] Template seeded for "${space}": 3 prompt shapes`);
}
export const crowdsurfModule: RSpaceModule = {
id: "crowdsurf",
name: "CrowdSurf",
icon: "🏄",
description: "Swipe-based community activity coordination",
scoping: { defaultScope: 'space', userConfigurable: false },
routes,
standaloneDomain: "crowdsurf.online",
landingPage: renderLanding,
seedTemplate: seedTemplateCrowdSurf,
feeds: [
{
id: "activity-triggers",
name: "Activity Triggers",
kind: "governance",
description: "Activity proposals and triggered events",
emits: ["folk-crowdsurf-prompt"],
},
],
acceptsFeeds: ["data", "governance"],
outputPaths: [
{ path: "prompts", name: "Prompts", icon: "🏄", description: "Active activity proposals" },
{ path: "triggered", name: "Triggered", icon: "🚀", description: "Activities that reached their threshold" },
],
};