diff --git a/modules/rvote/components/vote-demo.ts b/modules/rvote/components/vote-demo.ts index fdfb804..c38e0da 100644 --- a/modules/rvote/components/vote-demo.ts +++ b/modules/rvote/components/vote-demo.ts @@ -177,3 +177,133 @@ resetBtn.addEventListener("click", async () => { // ── Connect ── sync.connect(); + +// ═══════════════════════════════════════════════════════════ +// Conviction Voting Simulator (local-only, no sync) +// ═══════════════════════════════════════════════════════════ + +interface SimProposal { + id: string; + title: string; + description: string; + baseScore: number; + myVotes: number; // how many times user has upvoted (0..5) +} + +const MAX_CREDITS = 50; +let simCredits = MAX_CREDITS; +let simProposals: SimProposal[] = getInitialProposals(); + +function getInitialProposals(): SimProposal[] { + return [ + { id: "s1", title: "Add dark mode across all r* modules", description: "CSS custom properties for consistent theming", baseScore: 45, myVotes: 0 }, + { id: "s2", title: "Real-time collaboration in rNotes", description: "Automerge CRDT simultaneous editing", baseScore: 72, myVotes: 0 }, + { id: "s3", title: "Cosmolocal print-on-demand for merch", description: "Route orders to closest provider", baseScore: 38, myVotes: 0 }, + { id: "s4", title: "EncryptID passkeys for all auth", description: "One WebAuthn passkey, all apps", baseScore: 61, myVotes: 0 }, + { id: "s5", title: "Federated moderation tools", description: "Community-driven content policies", baseScore: 29, myVotes: 0 }, + ]; +} + +function quadraticCost(votes: number): number { + return votes * votes; +} + +function totalSpent(): number { + return simProposals.reduce((sum, p) => sum + quadraticCost(p.myVotes), 0); +} + +function effectiveScore(p: SimProposal): number { + return p.baseScore + p.myVotes; +} + +function costOfNextVote(p: SimProposal): number { + return quadraticCost(p.myVotes + 1) - quadraticCost(p.myVotes); +} + +function canAffordNextVote(p: SimProposal): boolean { + return costOfNextVote(p) <= simCredits && p.myVotes < 5; +} + +function renderSimulator(): void { + const simContainer = document.getElementById("rd-sim-proposals"); + const creditsEl = document.getElementById("rd-credits"); + if (!simContainer || !creditsEl) return; + + simCredits = MAX_CREDITS - totalSpent(); + creditsEl.textContent = String(simCredits); + creditsEl.style.color = simCredits < 10 ? "#ef4444" : simCredits < 20 ? "#f97316" : "#22c55e"; + + // Sort by effective score descending + const sorted = [...simProposals].sort((a, b) => effectiveScore(b) - effectiveScore(a)); + + simContainer.innerHTML = sorted.map((p, rank) => { + const score = effectiveScore(p); + const mySpent = quadraticCost(p.myVotes); + const nextCost = costOfNextVote(p); + const canVote = canAffordNextVote(p); + const canUnvote = p.myVotes > 0; + const promotionPct = Math.min(100, (score / 100) * 100); + + return `
${esc(p.description)}
+ +These polls are synced in real-time across the entire r* ecosystem via rSpace. Vote on options and watch tallies update live for everyone.
-