/** * Choices module — voting, ranking, and multi-criteria scoring tools. * * The folk-choice-* web components live in lib/ (shared with canvas). * This module provides: * - A page listing choice shapes in the current space * - API to query choice shapes from the Automerge store * - Links to create new choices on the canvas */ 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/choices — list choice shapes in the current space routes.get("/api/choices", async (c) => { const space = c.req.param("space") || c.req.query("space") || "demo"; const docData = getDocumentData(space); if (!docData?.shapes) { return c.json({ choices: [], total: 0 }); } const choiceTypes = ["folk-choice-vote", "folk-choice-rank", "folk-choice-spider"]; const choices: any[] = []; for (const [id, shape] of Object.entries(docData.shapes as Record)) { if (shape.forgotten) continue; if (choiceTypes.includes(shape.type)) { choices.push({ id, type: shape.type, title: shape.title || "Untitled", mode: shape.mode, optionCount: (shape.options || []).length, voteCount: (shape.votes || shape.rankings || shape.scores || []).length, createdAt: shape.createdAt, }); } } return c.json({ choices, total: choices.length }); }); // GET / — choices page routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; return c.html(renderShell({ title: `${spaceSlug} — Choices | rSpace`, moduleId: "rchoices", spaceSlug, modules: getModuleInfoList(), theme: "dark", body: ``, scripts: ``, styles: ``, })); }); // ── 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) .filter((s: any) => !s.forgotten && choiceTypes.includes(s.type)); if (existing.length > 0) return; } const now = Date.now(); const shapes: Record[] = [ { 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", icon: "☑", description: "Polls, rankings, and multi-criteria scoring", scoping: { defaultScope: 'space', userConfigurable: false }, routes, standaloneDomain: "rchoices.online", landingPage: renderLanding, seedTemplate: seedTemplateChoices, feeds: [ { id: "poll-results", name: "Poll Results", kind: "governance", description: "Live poll, ranking, and scoring outcomes", emits: ["folk-choice-vote", "folk-choice-rank", "folk-choice-spider"], }, ], acceptsFeeds: ["data", "governance"], outputPaths: [ { path: "polls", name: "Polls", icon: "☑️", description: "Active and closed polls" }, { path: "results", name: "Results", icon: "📊", description: "Poll and scoring results" }, ], };