452 lines
20 KiB
TypeScript
452 lines
20 KiB
TypeScript
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, 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,
|
|
Users,
|
|
Scale,
|
|
Clock,
|
|
Coins,
|
|
CheckCircle,
|
|
Shield,
|
|
Play,
|
|
ListOrdered,
|
|
Target,
|
|
Layers,
|
|
} from "lucide-react";
|
|
|
|
export default async function HomePage() {
|
|
const session = await auth();
|
|
|
|
// Get top ranking proposals
|
|
const proposals = await prisma.proposal.findMany({
|
|
where: { status: "RANKING" },
|
|
orderBy: { score: "desc" },
|
|
take: 5,
|
|
include: {
|
|
author: {
|
|
select: { id: true, name: true, email: true },
|
|
},
|
|
votes: true,
|
|
},
|
|
});
|
|
|
|
// Get user's votes and credits if logged in
|
|
let availableCredits = 0;
|
|
let userVotes: { proposalId: string; weight: number; effectiveWeight: number }[] = [];
|
|
|
|
if (session?.user?.id) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: session.user.id },
|
|
select: { credits: true, lastCreditAt: true },
|
|
});
|
|
|
|
if (user) {
|
|
availableCredits = calculateAvailableCredits(user.credits, user.lastCreditAt);
|
|
}
|
|
|
|
const votes = await prisma.vote.findMany({
|
|
where: {
|
|
userId: session.user.id,
|
|
proposalId: { in: proposals.map((p) => p.id) },
|
|
},
|
|
});
|
|
|
|
userVotes = votes.map((v) => ({
|
|
proposalId: v.proposalId,
|
|
weight: v.weight,
|
|
effectiveWeight: getEffectiveWeight(v.weight, v.createdAt),
|
|
}));
|
|
}
|
|
|
|
// Get counts for stats
|
|
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-16">
|
|
{/* Hero section */}
|
|
<section className="relative text-center py-16 space-y-6 overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-accent/10 -z-10" />
|
|
<div className="absolute top-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl -z-10" />
|
|
<div className="absolute bottom-0 left-0 w-96 h-96 bg-accent/5 rounded-full blur-3xl -z-10" />
|
|
|
|
<Badge variant="secondary" className="text-sm px-4 py-1 bg-primary/10 text-primary border-primary/20">
|
|
Part of the rSpace Ecosystem
|
|
</Badge>
|
|
<h1 className="text-5xl md:text-6xl font-bold tracking-tight max-w-4xl mx-auto leading-tight">
|
|
Democratic Backlog{" "}
|
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent">Prioritization</span>
|
|
</h1>
|
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
|
rVote uses <strong className="text-foreground">quadratic ranking</strong> to let your community democratically
|
|
prioritize proposals. The best ideas rise to the top through collective intelligence,
|
|
then advance to final voting.
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row justify-center gap-4 pt-4">
|
|
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70">
|
|
<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 border-primary/30 hover:bg-primary/5">
|
|
<Link href="/auth/signup">Create Account</Link>
|
|
</Button>
|
|
) : (
|
|
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
|
|
<Link href="/proposals">Browse Proposals</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* What is Quadratic Ranking */}
|
|
<section className="py-8">
|
|
<div className="text-center mb-12">
|
|
<Badge variant="outline" className="mb-4 border-primary/30">The Core Concept</Badge>
|
|
<h2 className="text-3xl font-bold mb-4">What is Quadratic Ranking?</h2>
|
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
A system where expressing <em>strong</em> preference costs progressively more,
|
|
creating a fair and balanced priority list that reflects true community consensus.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
|
<Card className="border-2 border-destructive/20 bg-destructive/5">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Scale className="h-5 w-5 text-destructive" />
|
|
The Problem
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 text-muted-foreground">
|
|
<p>
|
|
Traditional priority systems let those with more time, resources, or influence
|
|
dominate what gets attention.
|
|
</p>
|
|
<ul className="list-disc list-inside space-y-1">
|
|
<li>Loudest voices set the agenda</li>
|
|
<li>Important but less flashy ideas get buried</li>
|
|
<li>No way to express intensity of preference</li>
|
|
<li>Backlogs become political battlegrounds</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-accent/5">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<CheckCircle className="h-5 w-5 text-primary" />
|
|
The Solution: Quadratic Ranking
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 text-muted-foreground">
|
|
<p>
|
|
Quadratic ranking 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, 3 = 9</li>
|
|
<li>Everyone can participate meaningfully</li>
|
|
<li>Express strong opinions, but at a cost</li>
|
|
<li>Naturally surfaces community consensus</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 hover:bg-primary/5 transition-all duration-200"
|
|
>
|
|
<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 text-accent">{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">
|
|
Spreading votes across proposals you support is more efficient than concentrating on one.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* How it creates a democratic backlog */}
|
|
<section className="py-8 bg-gradient-to-r from-primary/5 via-transparent to-accent/5 -mx-4 px-4 rounded-xl">
|
|
<div className="text-center mb-12">
|
|
<Badge className="mb-4 bg-primary/10 text-primary border-primary/20">How It Works</Badge>
|
|
<h2 className="text-3xl font-bold mb-4">From Chaos to Consensus</h2>
|
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
Transform your community's ideas into a democratically prioritized backlog
|
|
through two simple stages.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
|
<Card className="border-primary/20 bg-card/80 backdrop-blur">
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center">
|
|
<ListOrdered className="h-5 w-5 text-primary-foreground" />
|
|
</div>
|
|
<div>
|
|
<Badge className="mb-1 bg-primary/10 text-primary">Stage 1</Badge>
|
|
<CardTitle>Quadratic 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 text-primary" />
|
|
<span>All proposals enter the ranking pool</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<Coins className="h-4 w-4 mt-1 shrink-0 text-primary" />
|
|
<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 text-primary" />
|
|
<span>Votes decay over 30-60 days</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<Target className="h-4 w-4 mt-1 shrink-0 text-primary" />
|
|
<span>Creates a living priority queue</span>
|
|
</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-accent/30 bg-gradient-to-br from-accent/10 to-primary/10">
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-accent to-primary flex items-center justify-center">
|
|
<ArrowRight className="h-5 w-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<Badge variant="secondary" className="mb-1 bg-accent/20 text-accent-foreground">
|
|
Threshold
|
|
</Badge>
|
|
<CardTitle>Score +100</CardTitle>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="text-muted-foreground">
|
|
<p>
|
|
When a proposal reaches a score of <strong className="text-foreground">+100</strong>, it
|
|
automatically advances to the final voting stage.
|
|
</p>
|
|
<p className="mt-2 text-sm">
|
|
This ensures only proposals with genuine community support move
|
|
forward for implementation decisions.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-secondary/30 bg-card/80 backdrop-blur">
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-secondary to-secondary/60 flex items-center justify-center">
|
|
<Vote className="h-5 w-5 text-secondary-foreground" />
|
|
</div>
|
|
<div>
|
|
<Badge variant="outline" className="mb-1 border-secondary/30">
|
|
Stage 2
|
|
</Badge>
|
|
<CardTitle>Pass/Fail Vote</CardTitle>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="text-muted-foreground">
|
|
<ul className="space-y-2">
|
|
<li className="flex items-start gap-2">
|
|
<Vote className="h-4 w-4 mt-1 shrink-0 text-secondary" />
|
|
<span>Yes / No / Abstain voting</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<Users className="h-4 w-4 mt-1 shrink-0 text-secondary" />
|
|
<span>One member = one vote</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<Clock className="h-4 w-4 mt-1 shrink-0 text-secondary" />
|
|
<span>7-day voting period</span>
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<CheckCircle className="h-4 w-4 mt-1 shrink-0 text-secondary" />
|
|
<span>Majority decides implementation</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 Prioritization</h2>
|
|
<p className="text-muted-foreground">Everything you need for democratic backlog management</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center mx-auto mb-3">
|
|
<Coins className="h-6 w-6 text-white" />
|
|
</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 className="border-primary/20 hover:border-primary/40 transition-colors">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center mx-auto mb-3">
|
|
<Clock className="h-6 w-6 text-white" />
|
|
</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 className="border-primary/20 hover:border-primary/40 transition-colors">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center mx-auto mb-3">
|
|
<Shield className="h-6 w-6 text-white" />
|
|
</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 className="border-primary/20 hover:border-primary/40 transition-colors">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center mx-auto mb-3">
|
|
<Zap className="h-6 w-6 text-white" />
|
|
</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>
|
|
|
|
{/* Stats */}
|
|
{(rankingCount > 0 || userCount > 1) && (
|
|
<section className="py-8">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<Card className="bg-gradient-to-br from-primary/10 to-transparent border-primary/20">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="text-3xl font-bold text-primary">{userCount}</div>
|
|
<p className="text-sm text-muted-foreground">Members</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-gradient-to-br from-accent/10 to-transparent border-accent/20">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="text-3xl font-bold text-accent">{rankingCount}</div>
|
|
<p className="text-sm text-muted-foreground">Being Ranked</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-gradient-to-br from-secondary/10 to-transparent border-secondary/20">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="text-3xl font-bold text-secondary">{votingCount}</div>
|
|
<p className="text-sm text-muted-foreground">In Voting</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-gradient-to-br from-green-500/10 to-transparent border-green-500/20">
|
|
<CardContent className="pt-6 text-center">
|
|
<div className="text-3xl font-bold text-green-600">{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" className="text-primary hover:text-primary/80">
|
|
<Link href="/proposals">
|
|
View All <ArrowRight className="ml-2 h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
<ProposalList
|
|
proposals={proposals}
|
|
userVotes={userVotes}
|
|
availableCredits={availableCredits}
|
|
/>
|
|
</section>
|
|
)}
|
|
|
|
{/* CTA */}
|
|
<section className="py-12">
|
|
<Card className="border-2 border-primary/30 bg-gradient-to-br from-primary/10 via-accent/5 to-secondary/10 overflow-hidden relative">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/10 rounded-full blur-3xl" />
|
|
<div className="absolute bottom-0 left-0 w-64 h-64 bg-accent/10 rounded-full blur-3xl" />
|
|
<CardContent className="py-12 text-center space-y-6 relative">
|
|
<Badge className="bg-primary/10 text-primary border-primary/20">Join the rSpace Ecosystem</Badge>
|
|
<h2 className="text-3xl font-bold">Ready to prioritize democratically?</h2>
|
|
<p className="text-lg text-muted-foreground max-w-xl mx-auto">
|
|
Experience quadratic ranking firsthand. Try the interactive demo or
|
|
create an account to start building your community's backlog together.
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
|
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-accent hover:opacity-90">
|
|
<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 border-primary/30 hover:bg-primary/5">
|
|
<Link href="/auth/signup">
|
|
Create Free Account
|
|
<ArrowRight className="ml-2 h-5 w-5" />
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|