diff --git a/src/app/demo/page.tsx b/src/app/demo/page.tsx new file mode 100644 index 0000000..fcd4bfd --- /dev/null +++ b/src/app/demo/page.tsx @@ -0,0 +1,446 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import Link from "next/link"; +import { + ChevronUp, + ChevronDown, + Check, + X, + Minus, + ArrowRight, + RotateCcw, + Coins, + TrendingUp, + Clock, + Users, +} from "lucide-react"; + +interface DemoProposal { + id: number; + title: string; + description: string; + score: number; + userVote: number; + stage: "ranking" | "voting"; + yesVotes: number; + noVotes: number; +} + +const initialProposals: DemoProposal[] = [ + { + id: 1, + title: "Add dark mode to the dashboard", + description: "Implement a dark theme option for better nighttime usage", + score: 87, + userVote: 0, + stage: "ranking", + yesVotes: 0, + noVotes: 0, + }, + { + id: 2, + title: "Weekly community calls", + description: "Host weekly video calls to discuss proposals and progress", + score: 42, + userVote: 0, + stage: "ranking", + yesVotes: 0, + noVotes: 0, + }, + { + id: 3, + title: "Create a mobile app", + description: "Build native iOS and Android apps for on-the-go voting", + score: 103, + userVote: 0, + stage: "voting", + yesVotes: 12, + noVotes: 5, + }, +]; + +export default function DemoPage() { + const [credits, setCredits] = useState(100); + const [proposals, setProposals] = useState(initialProposals); + const [voteWeight, setVoteWeight] = useState(1); + const [activeProposal, setActiveProposal] = useState(null); + + const voteCost = voteWeight * voteWeight; + const maxWeight = Math.floor(Math.sqrt(credits)); + + function castVote(proposalId: number, direction: "up" | "down") { + const weight = direction === "up" ? voteWeight : -voteWeight; + const cost = voteCost; + + if (cost > credits) return; + + setProposals((prev) => + prev.map((p) => { + if (p.id === proposalId) { + // Return old vote credits if changing vote + const oldCost = p.userVote !== 0 ? p.userVote * p.userVote : 0; + const newScore = p.score - p.userVote + weight; + + // Check if promoted + const promoted = newScore >= 100 && p.stage === "ranking"; + + return { + ...p, + score: newScore, + userVote: weight, + stage: promoted ? "voting" : p.stage, + yesVotes: promoted ? 8 : p.yesVotes, + noVotes: promoted ? 3 : p.noVotes, + }; + } + return p; + }) + ); + + // Deduct credits (simplified - doesn't return old vote credits in demo) + setCredits((prev) => prev - cost); + setActiveProposal(null); + } + + function castFinalVote(proposalId: number, vote: "yes" | "no" | "abstain") { + setProposals((prev) => + prev.map((p) => { + if (p.id === proposalId) { + return { + ...p, + yesVotes: vote === "yes" ? p.yesVotes + 1 : p.yesVotes, + noVotes: vote === "no" ? p.noVotes + 1 : p.noVotes, + }; + } + return p; + }) + ); + } + + function resetDemo() { + setCredits(100); + setProposals(initialProposals); + setVoteWeight(1); + setActiveProposal(null); + } + + const rankingProposals = proposals.filter((p) => p.stage === "ranking"); + const votingProposals = proposals.filter((p) => p.stage === "voting"); + + return ( +
+
+ + Interactive Demo + +

Try Quadratic Voting

+

+ Experience how rVote works without creating an account. Vote on these + sample proposals and see the quadratic cost in action. +

+
+ + {/* Credits display */} + + +
+
+
+ + {credits} credits +
+ + Max vote weight: {maxWeight} + +
+ +
+
+
+ + {/* Quadratic cost explainer */} + + + + + Quadratic Voting Cost + + + The more votes you put on one proposal, the more each additional + vote costs + + + +
+ {[1, 2, 3, 4, 5].map((w) => ( +
+
{w}
+
vote{w > 1 ? "s" : ""}
+
{w * w} credits
+
+ ))} +
+

+ This prevents wealthy voters from dominating. Spreading votes across + proposals is more efficient than concentrating them. +

+
+
+ + {/* Ranking stage */} +
+
+ Stage 1 +

Ranking

+ + Proposals need +100 to advance + +
+ + {rankingProposals.map((proposal) => ( + +
+
+ + + {proposal.score} + + +
+
+

{proposal.title}

+

+ {proposal.description} +

+
+
+ Progress to voting + {proposal.score}/100 +
+ +
+ {proposal.userVote !== 0 && ( +

+ Your vote: {proposal.userVote > 0 ? "+" : ""} + {proposal.userVote} ({Math.abs(proposal.userVote * proposal.userVote)} credits) +

+ )} +
+
+ + {/* Vote weight selector */} + {(activeProposal === proposal.id || + activeProposal === -proposal.id) && ( +
+
+ + + setVoteWeight( + Math.max(1, Math.min(maxWeight, parseInt(e.target.value) || 1)) + ) + } + className="w-20" + /> + + Cost: {voteCost} credits + +
+ + +
+
+ )} + + ))} +
+ + {/* Voting stage */} +
+
+ Stage 2 +

Pass/Fail Voting

+ + One member = one vote + +
+ + {votingProposals.map((proposal) => { + const total = proposal.yesVotes + proposal.noVotes; + const yesPercent = total > 0 ? (proposal.yesVotes / total) * 100 : 50; + return ( + + +
+ {proposal.title} +
+ + 6 days left +
+
+ {proposal.description} +
+ + {/* Vote bar */} +
+
+
+
+
+
+ + {proposal.yesVotes} Yes ({Math.round(yesPercent)}%) + + + {proposal.noVotes} No ({Math.round(100 - yesPercent)}%) + +
+
+ + {/* Vote buttons */} +
+ + + +
+ + + ); + })} +
+ + {/* CTA */} + + +

Ready to try it for real?

+

+ Create an account to start ranking and voting on community proposals. + You'll get 50 credits to start and earn 10 more each day. +

+
+ + +
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 84c25b4..f4f94f3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,12 +2,24 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { ProposalList } from "@/components/ProposalList"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { calculateAvailableCredits } from "@/lib/credits"; import { getEffectiveWeight } from "@/lib/voting"; import Link from "next/link"; -import { ArrowRight, TrendingUp, Vote, Zap } from "lucide-react"; +import { + ArrowRight, + TrendingUp, + Vote, + Zap, + Users, + Scale, + Clock, + Coins, + CheckCircle, + Shield, + Play, +} from "lucide-react"; export default async function HomePage() { const session = await auth(); @@ -16,7 +28,7 @@ export default async function HomePage() { const proposals = await prisma.proposal.findMany({ where: { status: "RANKING" }, orderBy: { score: "desc" }, - take: 10, + take: 5, include: { author: { select: { id: true, name: true, email: true }, @@ -54,156 +66,363 @@ export default async function HomePage() { } // Get counts for stats - const [rankingCount, votingCount, passedCount] = await Promise.all([ + const [rankingCount, votingCount, passedCount, userCount] = await Promise.all([ prisma.proposal.count({ where: { status: "RANKING" } }), prisma.proposal.count({ where: { status: "VOTING" } }), prisma.proposal.count({ where: { status: "PASSED" } }), + prisma.user.count(), ]); return ( -
+
{/* Hero section */} -
-

- Community-Driven Governance +
+ + Quadratic Voting for Communities + +

+ Democratic Governance,{" "} + Reimagined

- Rank proposals using quadratic voting. Top proposals advance to pass/fail voting. - Your voice matters more when you concentrate your votes. + rVote uses quadratic voting to give every voice weight while preventing + any single actor from dominating. Proposals are ranked by the community, + and the best ideas rise to the top.

- {!session?.user && ( -
- + {!session?.user ? ( + - -
- )} - {session?.user && ( -
- - -
- )} + )} +

- {/* Stats */} -
- - - Ranking - - - -
{rankingCount}
-

proposals being ranked

-
-
- - - Voting - - - -
{votingCount}
-

proposals in pass/fail voting

-
-
- - - Passed - - - -
{passedCount}
-

proposals approved

-
-
-
- - {/* How it works */} + {/* What is Quadratic Voting */}
-

How It Works

-
- +
+

What is Quadratic Voting?

+

+ A voting system where the cost of each additional vote increases + quadratically, making it expensive to dominate but cheap to participate. +

+
+ +
+ - Stage 1 - Ranking + + + The Problem with Traditional Voting + - +

- Proposals start in the ranking stage. Use your credits to upvote or - downvote. Quadratic voting: 1 vote costs 1 credit, - 2 votes cost 4 credits, 3 votes cost 9 credits. + In traditional systems, those with more resources (time, money, + influence) can easily dominate outcomes.

+
    +
  • One vote per person ignores intensity of preference
  • +
  • Unlimited voting lets whales control results
  • +
  • Small voices get drowned out
  • +
+ + + + + + The Quadratic Solution + + + +

+ Quadratic voting balances participation and conviction by making + additional votes progressively more expensive. +

+
    +
  • 1 vote = 1 credit, 2 votes = 4 credits, 3 = 9
  • +
  • Express strong opinions, but at a cost
  • +
  • More voices, more balanced outcomes
  • +
+
+
+
+ + {/* Visual cost table */} +
+

+ Vote Cost Calculator +

+
+ {[1, 2, 3, 4, 5].map((votes) => ( +
+
{votes}
+
+ vote{votes > 1 ? "s" : ""} +
+
{votes * votes}
+
credits
+
+ ))} +
+

+ It's more efficient to spread votes across proposals you support + than to concentrate them on one. +

+
+
+ + {/* How it works - 2 stages */} +
+
+

Two-Stage Voting Process

+

+ Proposals go through ranking before reaching a final vote, ensuring + only well-supported ideas get full consideration. +

+
+ +
- - Threshold - - Score +100 +
+
+ 1 +
+
+ Stage 1 + Ranking +
+
+
+ +
    +
  • + + Proposals start here +
  • +
  • + + Upvote/downvote with quadratic cost +
  • +
  • + + Votes decay over 30-60 days +
  • +
+
+
+ + + +
+
+ +
+
+ + Threshold + + Score +100 +
+

When a proposal reaches a score of +100, it - automatically advances to the pass/fail voting stage. Old votes - decay over time, keeping rankings fresh. + automatically advances to the final voting stage. +

+

+ This ensures only proposals with genuine community support move + forward.

+ - - Stage 2 - - Pass/Fail +
+
+ 2 +
+
+ + Stage 2 + + Pass/Fail +
+
-

- In the final stage, members vote Yes, No, or Abstain. - Voting is open for 7 days. Simple majority wins. One member = one vote. +

    +
  • + + Yes / No / Abstain voting +
  • +
  • + + One member = one vote +
  • +
  • + + 7-day voting period +
  • +
+
+
+
+
+ + {/* Features */} +
+
+

Built for Fair Governance

+
+ +
+ + +
+ +
+

Earn Credits Daily

+

+ Get 10 credits every day. Start with 50. Max 500. +

+
+
+ + + +
+ +
+

Vote Decay

+

+ Old votes fade away, keeping rankings fresh and dynamic. +

+
+
+ + + +
+ +
+

Sybil Resistant

+

+ Quadratic costs make fake account attacks expensive. +

+
+
+ + + +
+ +
+

Auto Promotion

+

+ Top proposals automatically advance to voting.

- {/* Top proposals */} -
-
-

Top Proposals

- -
- {proposals.length > 0 ? ( + {/* Stats */} + {(rankingCount > 0 || userCount > 1) && ( +
+
+ + +
{userCount}
+

Members

+
+
+ + +
{rankingCount}
+

Being Ranked

+
+
+ + +
{votingCount}
+

In Voting

+
+
+ + +
{passedCount}
+

Passed

+
+
+
+
+ )} + + {/* Active proposals */} + {proposals.length > 0 && ( +
+
+

Active Proposals

+ +
- ) : ( - - -

No proposals yet. Be the first to create one!

- {session?.user && ( -
+ )} + + {/* CTA */} +
+ + +

Ready to give it a try?

+

+ Experience quadratic voting firsthand. Try the interactive demo or + create an account to start participating in community governance. +

+
+ + {!session?.user && ( + )} - - - )} +
+
+
);