Add interactive demo page and update landing page
- Add /demo route with interactive quadratic voting demonstration - Users can try voting without creating an account - Sample proposals show both ranking and pass/fail voting stages - Update landing page with comprehensive quadratic voting explainer - Add "What is Quadratic Voting?" section with problem/solution - Add vote cost calculator showing quadratic progression - Add two-stage voting process explanation - Add feature highlights (credits, decay, sybil resistance, auto-promotion) - Add call-to-action sections linking to demo and signup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c6b7f5d899
commit
f8bf201c7a
|
|
@ -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<DemoProposal[]>(initialProposals);
|
||||
const [voteWeight, setVoteWeight] = useState(1);
|
||||
const [activeProposal, setActiveProposal] = useState<number | null>(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 (
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
<div className="text-center space-y-4">
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
Interactive Demo
|
||||
</Badge>
|
||||
<h1 className="text-4xl font-bold">Try Quadratic Voting</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Experience how rVote works without creating an account. Vote on these
|
||||
sample proposals and see the quadratic cost in action.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Credits display */}
|
||||
<Card className="border-primary/50 bg-primary/5">
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className="h-5 w-5 text-primary" />
|
||||
<span className="font-semibold text-lg">{credits} credits</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Max vote weight: {maxWeight}
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={resetDemo}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Reset Demo
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quadratic cost explainer */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Quadratic Voting Cost
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
The more votes you put on one proposal, the more each additional
|
||||
vote costs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-5 gap-2 text-center text-sm">
|
||||
{[1, 2, 3, 4, 5].map((w) => (
|
||||
<div
|
||||
key={w}
|
||||
className={`p-3 rounded-lg border ${
|
||||
w <= maxWeight
|
||||
? "bg-primary/10 border-primary/30"
|
||||
: "bg-muted border-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-lg">{w}</div>
|
||||
<div className="text-muted-foreground">vote{w > 1 ? "s" : ""}</div>
|
||||
<div className="font-mono text-xs mt-1">{w * w} credits</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4 text-center">
|
||||
This prevents wealthy voters from dominating. Spreading votes across
|
||||
proposals is more efficient than concentrating them.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Ranking stage */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>Stage 1</Badge>
|
||||
<h2 className="text-xl font-semibold">Ranking</h2>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Proposals need +100 to advance
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{rankingProposals.map((proposal) => (
|
||||
<Card key={proposal.id}>
|
||||
<div className="flex">
|
||||
<div className="flex flex-col items-center justify-center px-4 border-r bg-muted/30 min-w-[80px]">
|
||||
<Button
|
||||
variant={proposal.userVote > 0 ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
if (proposal.userVote > 0) {
|
||||
// Remove vote (simplified)
|
||||
setProposals((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === proposal.id
|
||||
? { ...p, score: p.score - p.userVote, userVote: 0 }
|
||||
: p
|
||||
)
|
||||
);
|
||||
setCredits((c) => c + proposal.userVote * proposal.userVote);
|
||||
} else {
|
||||
setActiveProposal(proposal.id);
|
||||
}
|
||||
}}
|
||||
disabled={credits < 1 && proposal.userVote === 0}
|
||||
>
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
</Button>
|
||||
<Badge variant="outline" className="font-mono text-lg my-1">
|
||||
{proposal.score}
|
||||
</Badge>
|
||||
<Button
|
||||
variant={proposal.userVote < 0 ? "destructive" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
if (proposal.userVote < 0) {
|
||||
setProposals((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === proposal.id
|
||||
? { ...p, score: p.score - p.userVote, userVote: 0 }
|
||||
: p
|
||||
)
|
||||
);
|
||||
setCredits((c) => c + proposal.userVote * proposal.userVote);
|
||||
} else {
|
||||
setActiveProposal(-proposal.id); // Negative for downvote
|
||||
}
|
||||
}}
|
||||
disabled={credits < 1 && proposal.userVote === 0}
|
||||
>
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 p-4">
|
||||
<h3 className="font-semibold">{proposal.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{proposal.description}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span>Progress to voting</span>
|
||||
<span>{proposal.score}/100</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={Math.min((proposal.score / 100) * 100, 100)}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
{proposal.userVote !== 0 && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Your vote: {proposal.userVote > 0 ? "+" : ""}
|
||||
{proposal.userVote} ({Math.abs(proposal.userVote * proposal.userVote)} credits)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vote weight selector */}
|
||||
{(activeProposal === proposal.id ||
|
||||
activeProposal === -proposal.id) && (
|
||||
<div className="border-t p-4 bg-muted/30">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label>Vote weight:</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={maxWeight}
|
||||
value={voteWeight}
|
||||
onChange={(e) =>
|
||||
setVoteWeight(
|
||||
Math.max(1, Math.min(maxWeight, parseInt(e.target.value) || 1))
|
||||
)
|
||||
}
|
||||
className="w-20"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Cost: {voteCost} credits
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setActiveProposal(null)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={activeProposal > 0 ? "default" : "destructive"}
|
||||
onClick={() =>
|
||||
castVote(
|
||||
Math.abs(activeProposal),
|
||||
activeProposal > 0 ? "up" : "down"
|
||||
)
|
||||
}
|
||||
disabled={voteCost > credits}
|
||||
>
|
||||
{activeProposal > 0 ? "Upvote" : "Downvote"} ({voteCost} credits)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Voting stage */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Stage 2</Badge>
|
||||
<h2 className="text-xl font-semibold">Pass/Fail Voting</h2>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
One member = one vote
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{votingProposals.map((proposal) => {
|
||||
const total = proposal.yesVotes + proposal.noVotes;
|
||||
const yesPercent = total > 0 ? (proposal.yesVotes / total) * 100 : 50;
|
||||
return (
|
||||
<Card key={proposal.id}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{proposal.title}</CardTitle>
|
||||
<div className="flex items-center gap-1 text-sm text-yellow-600">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>6 days left</span>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription>{proposal.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Vote bar */}
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 rounded-full overflow-hidden bg-red-500/20 flex">
|
||||
<div
|
||||
className="h-full bg-green-500 transition-all"
|
||||
style={{ width: `${yesPercent}%` }}
|
||||
/>
|
||||
<div
|
||||
className="h-full bg-red-500 transition-all"
|
||||
style={{ width: `${100 - yesPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-600">
|
||||
{proposal.yesVotes} Yes ({Math.round(yesPercent)}%)
|
||||
</span>
|
||||
<span className="text-red-600">
|
||||
{proposal.noVotes} No ({Math.round(100 - yesPercent)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vote buttons */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-col h-auto py-3 hover:bg-green-500/10 hover:border-green-500"
|
||||
onClick={() => castFinalVote(proposal.id, "yes")}
|
||||
>
|
||||
<Check className="h-5 w-5 text-green-500" />
|
||||
<span className="text-xs mt-1">Yes</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-col h-auto py-3 hover:bg-red-500/10 hover:border-red-500"
|
||||
onClick={() => castFinalVote(proposal.id, "no")}
|
||||
>
|
||||
<X className="h-5 w-5 text-red-500" />
|
||||
<span className="text-xs mt-1">No</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-col h-auto py-3"
|
||||
onClick={() => castFinalVote(proposal.id, "abstain")}
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
<span className="text-xs mt-1">Abstain</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<Card className="border-primary bg-primary/5">
|
||||
<CardContent className="py-8 text-center space-y-4">
|
||||
<h2 className="text-2xl font-bold">Ready to try it for real?</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Create an account to start ranking and voting on community proposals.
|
||||
You'll get 50 credits to start and earn 10 more each day.
|
||||
</p>
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/auth/signup">
|
||||
Create Account <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild size="lg">
|
||||
<Link href="/">Learn More</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
429
src/app/page.tsx
429
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 (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-16">
|
||||
{/* Hero section */}
|
||||
<section className="text-center py-12 space-y-4">
|
||||
<h1 className="text-4xl font-bold tracking-tight">
|
||||
Community-Driven Governance
|
||||
<section className="text-center py-16 space-y-6">
|
||||
<Badge variant="secondary" className="text-sm px-4 py-1">
|
||||
Quadratic Voting for Communities
|
||||
</Badge>
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight max-w-4xl mx-auto leading-tight">
|
||||
Democratic Governance,{" "}
|
||||
<span className="text-primary">Reimagined</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
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.
|
||||
</p>
|
||||
{!session?.user && (
|
||||
<div className="flex justify-center gap-4 pt-4">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/auth/signup">Get Started</Link>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4 pt-4">
|
||||
<Button asChild size="lg" className="text-lg px-8">
|
||||
<Link href="/demo">
|
||||
<Play className="mr-2 h-5 w-5" />
|
||||
Try the Demo
|
||||
</Link>
|
||||
</Button>
|
||||
{!session?.user ? (
|
||||
<Button asChild variant="outline" size="lg" className="text-lg px-8">
|
||||
<Link href="/auth/signup">Create Account</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
) : (
|
||||
<Button asChild variant="outline" size="lg" className="text-lg px-8">
|
||||
<Link href="/proposals">Browse Proposals</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{session?.user && (
|
||||
<div className="flex justify-center gap-4 pt-4">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/proposals/new">Create Proposal</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/voting">View Active Votes</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats */}
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Ranking</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{rankingCount}</div>
|
||||
<p className="text-xs text-muted-foreground">proposals being ranked</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Voting</CardTitle>
|
||||
<Vote className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{votingCount}</div>
|
||||
<p className="text-xs text-muted-foreground">proposals in pass/fail voting</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Passed</CardTitle>
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{passedCount}</div>
|
||||
<p className="text-xs text-muted-foreground">proposals approved</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* How it works */}
|
||||
{/* What is Quadratic Voting */}
|
||||
<section className="py-8">
|
||||
<h2 className="text-2xl font-bold mb-6">How It Works</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold mb-4">What is Quadratic Voting?</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
A voting system where the cost of each additional vote increases
|
||||
quadratically, making it expensive to dominate but cheap to participate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
<Card className="border-2">
|
||||
<CardHeader>
|
||||
<Badge className="w-fit mb-2">Stage 1</Badge>
|
||||
<CardTitle>Ranking</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Scale className="h-5 w-5 text-primary" />
|
||||
The Problem with Traditional Voting
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground">
|
||||
<CardContent className="space-y-3 text-muted-foreground">
|
||||
<p>
|
||||
Proposals start in the ranking stage. Use your credits to upvote or
|
||||
downvote. <strong>Quadratic voting</strong>: 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.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>One vote per person ignores intensity of preference</li>
|
||||
<li>Unlimited voting lets whales control results</li>
|
||||
<li>Small voices get drowned out</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-2 border-primary/50 bg-primary/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-primary" />
|
||||
The Quadratic Solution
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-muted-foreground">
|
||||
<p>
|
||||
Quadratic voting balances participation and conviction by making
|
||||
additional votes progressively more expensive.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>1 vote = 1 credit, 2 votes = 4 credits, 3 = 9</li>
|
||||
<li>Express strong opinions, but at a cost</li>
|
||||
<li>More voices, more balanced outcomes</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Visual cost table */}
|
||||
<div className="mt-12 max-w-2xl mx-auto">
|
||||
<h3 className="text-lg font-semibold text-center mb-4">
|
||||
Vote Cost Calculator
|
||||
</h3>
|
||||
<div className="grid grid-cols-5 gap-2 text-center">
|
||||
{[1, 2, 3, 4, 5].map((votes) => (
|
||||
<div
|
||||
key={votes}
|
||||
className="p-4 rounded-lg border-2 bg-card hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<div className="text-2xl font-bold text-primary">{votes}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
vote{votes > 1 ? "s" : ""}
|
||||
</div>
|
||||
<div className="text-lg font-mono mt-2">{votes * votes}</div>
|
||||
<div className="text-xs text-muted-foreground">credits</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-sm text-muted-foreground mt-4">
|
||||
It's more efficient to spread votes across proposals you support
|
||||
than to concentrate them on one.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How it works - 2 stages */}
|
||||
<section className="py-8 bg-muted/30 -mx-4 px-4 rounded-xl">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold mb-4">Two-Stage Voting Process</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Proposals go through ranking before reaching a final vote, ensuring
|
||||
only well-supported ideas get full consideration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Badge className="w-fit mb-2" variant="secondary">
|
||||
Threshold
|
||||
</Badge>
|
||||
<CardTitle>Score +100</CardTitle>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="font-bold text-primary">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge className="mb-1">Stage 1</Badge>
|
||||
<CardTitle>Ranking</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground">
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<TrendingUp className="h-4 w-4 mt-1 shrink-0" />
|
||||
<span>Proposals start here</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Coins className="h-4 w-4 mt-1 shrink-0" />
|
||||
<span>Upvote/downvote with quadratic cost</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Clock className="h-4 w-4 mt-1 shrink-0" />
|
||||
<span>Votes decay over 30-60 days</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-primary/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center">
|
||||
<ArrowRight className="h-5 w-5 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<Badge variant="secondary" className="mb-1">
|
||||
Threshold
|
||||
</Badge>
|
||||
<CardTitle>Score +100</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground">
|
||||
<p>
|
||||
When a proposal reaches a score of <strong>+100</strong>, it
|
||||
automatically advances to the pass/fail voting stage. Old votes
|
||||
decay over time, keeping rankings fresh.
|
||||
automatically advances to the final voting stage.
|
||||
</p>
|
||||
<p className="mt-2 text-sm">
|
||||
This ensures only proposals with genuine community support move
|
||||
forward.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Badge className="w-fit mb-2" variant="outline">
|
||||
Stage 2
|
||||
</Badge>
|
||||
<CardTitle>Pass/Fail</CardTitle>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="font-bold text-primary">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-1">
|
||||
Stage 2
|
||||
</Badge>
|
||||
<CardTitle>Pass/Fail</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-muted-foreground">
|
||||
<p>
|
||||
In the final stage, members vote <strong>Yes, No, or Abstain</strong>.
|
||||
Voting is open for 7 days. Simple majority wins. One member = one vote.
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<Vote className="h-4 w-4 mt-1 shrink-0" />
|
||||
<span>Yes / No / Abstain voting</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Users className="h-4 w-4 mt-1 shrink-0" />
|
||||
<span>One member = one vote</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Clock className="h-4 w-4 mt-1 shrink-0" />
|
||||
<span>7-day voting period</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section className="py-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold mb-4">Built for Fair Governance</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-green-500/10 flex items-center justify-center mx-auto mb-3">
|
||||
<Coins className="h-6 w-6 text-green-500" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Earn Credits Daily</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get 10 credits every day. Start with 50. Max 500.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-blue-500/10 flex items-center justify-center mx-auto mb-3">
|
||||
<Clock className="h-6 w-6 text-blue-500" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Vote Decay</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Old votes fade away, keeping rankings fresh and dynamic.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-purple-500/10 flex items-center justify-center mx-auto mb-3">
|
||||
<Shield className="h-6 w-6 text-purple-500" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Sybil Resistant</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Quadratic costs make fake account attacks expensive.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-orange-500/10 flex items-center justify-center mx-auto mb-3">
|
||||
<Zap className="h-6 w-6 text-orange-500" />
|
||||
</div>
|
||||
<h3 className="font-semibold mb-1">Auto Promotion</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Top proposals automatically advance to voting.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Top proposals */}
|
||||
<section className="py-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">Top Proposals</h2>
|
||||
<Button asChild variant="ghost">
|
||||
<Link href="/proposals">
|
||||
View All <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{proposals.length > 0 ? (
|
||||
{/* Stats */}
|
||||
{(rankingCount > 0 || userCount > 1) && (
|
||||
<section className="py-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-3xl font-bold">{userCount}</div>
|
||||
<p className="text-sm text-muted-foreground">Members</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-3xl font-bold">{rankingCount}</div>
|
||||
<p className="text-sm text-muted-foreground">Being Ranked</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-3xl font-bold">{votingCount}</div>
|
||||
<p className="text-sm text-muted-foreground">In Voting</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="text-3xl font-bold">{passedCount}</div>
|
||||
<p className="text-sm text-muted-foreground">Passed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Active proposals */}
|
||||
{proposals.length > 0 && (
|
||||
<section className="py-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">Active Proposals</h2>
|
||||
<Button asChild variant="ghost">
|
||||
<Link href="/proposals">
|
||||
View All <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<ProposalList
|
||||
proposals={proposals}
|
||||
userVotes={userVotes}
|
||||
availableCredits={availableCredits}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center text-muted-foreground">
|
||||
<p>No proposals yet. Be the first to create one!</p>
|
||||
{session?.user && (
|
||||
<Button asChild className="mt-4">
|
||||
<Link href="/proposals/new">Create Proposal</Link>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-12">
|
||||
<Card className="border-2 border-primary/50 bg-gradient-to-br from-primary/5 to-primary/10">
|
||||
<CardContent className="py-12 text-center space-y-6">
|
||||
<h2 className="text-3xl font-bold">Ready to give it a try?</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-xl mx-auto">
|
||||
Experience quadratic voting firsthand. Try the interactive demo or
|
||||
create an account to start participating in community governance.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<Button asChild size="lg" className="text-lg px-8">
|
||||
<Link href="/demo">
|
||||
<Play className="mr-2 h-5 w-5" />
|
||||
Interactive Demo
|
||||
</Link>
|
||||
</Button>
|
||||
{!session?.user && (
|
||||
<Button asChild variant="outline" size="lg" className="text-lg px-8">
|
||||
<Link href="/auth/signup">
|
||||
Create Free Account
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue