146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
/**
|
|
* 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<string, any>)) {
|
|
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: `<folk-choices-dashboard space="${spaceSlug}"></folk-choices-dashboard>`,
|
|
scripts: `<script type="module" src="/modules/rchoices/folk-choices-dashboard.js"></script>`,
|
|
styles: `<link rel="stylesheet" href="/modules/rchoices/choices.css">`,
|
|
}));
|
|
});
|
|
|
|
// ── 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",
|
|
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" },
|
|
],
|
|
};
|