Fix demo page voting - use Button components and proper flex layout

- Replace raw <button> elements with Button components for proper hydration
- Use plain div instead of Card for proposal items to avoid flex-col conflicts
- Simplify vote handlers into handleUpvote/handleDownvote functions
- Reddit-style layout: vote column (72px) on left with border separator
- Orange upvotes, blue downvotes with proper hover/active states

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-05 22:34:54 +00:00
parent eebd6a4349
commit bc56e5bdb6
1 changed files with 171 additions and 198 deletions

View File

@ -4,7 +4,6 @@ import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import Link from "next/link"; import Link from "next/link";
import { import {
ChevronUp, ChevronUp,
@ -95,31 +94,53 @@ export default function DemoPage() {
const maxWeight = Math.floor(Math.sqrt(credits)); const maxWeight = Math.floor(Math.sqrt(credits));
function incrementVote(proposalId: number) { function handleUpvote(proposalId: number) {
setProposals((prev) => setProposals((prev) =>
prev.map((p) => { prev.map((p) => {
if (p.id === proposalId) { if (p.id !== proposalId) return p;
// If already voted up, remove the vote
if (p.userVote > 0) {
const refund = p.userVote * p.userVote;
setCredits((c) => c + refund);
return { ...p, score: p.score - p.userVote, userVote: 0, pendingVote: 0 };
}
// If already voted down, can't upvote
if (p.userVote < 0) return p;
// Increment pending vote
const newPending = p.pendingVote + 1; const newPending = p.pendingVote + 1;
const newCost = newPending * newPending; const newCost = newPending * newPending;
if (newCost <= credits) { if (newCost <= credits && newPending <= maxWeight) {
return { ...p, pendingVote: newPending }; return { ...p, pendingVote: newPending };
} }
}
return p; return p;
}) })
); );
} }
function decrementVote(proposalId: number) { function handleDownvote(proposalId: number) {
setProposals((prev) => setProposals((prev) =>
prev.map((p) => { prev.map((p) => {
if (p.id === proposalId) { if (p.id !== proposalId) return p;
// If already voted down, remove the vote
if (p.userVote < 0) {
const refund = p.userVote * p.userVote;
setCredits((c) => c + refund);
return { ...p, score: p.score - p.userVote, userVote: 0, pendingVote: 0 };
}
// If already voted up, can't downvote
if (p.userVote > 0) return p;
// Decrement pending vote
const newPending = p.pendingVote - 1; const newPending = p.pendingVote - 1;
const newCost = newPending * newPending; const newCost = newPending * newPending;
if (newCost <= credits) { if (newCost <= credits && Math.abs(newPending) <= maxWeight) {
return { ...p, pendingVote: newPending }; return { ...p, pendingVote: newPending };
} }
}
return p; return p;
}) })
); );
@ -131,10 +152,11 @@ export default function DemoPage() {
); );
} }
function castVote(proposalId: number) { function confirmVote(proposalId: number) {
setProposals((prev) => setProposals((prev) =>
prev.map((p) => { prev.map((p) => {
if (p.id === proposalId && p.pendingVote !== 0) { if (p.id !== proposalId || p.pendingVote === 0) return p;
const cost = p.pendingVote * p.pendingVote; const cost = p.pendingVote * p.pendingVote;
const newScore = p.score + p.pendingVote; const newScore = p.score + p.pendingVote;
const promoted = newScore >= 100 && p.stage === "ranking"; const promoted = newScore >= 100 && p.stage === "ranking";
@ -150,25 +172,6 @@ export default function DemoPage() {
yesVotes: promoted ? 8 : p.yesVotes, yesVotes: promoted ? 8 : p.yesVotes,
noVotes: promoted ? 3 : p.noVotes, noVotes: promoted ? 3 : p.noVotes,
}; };
}
return p;
})
);
}
function removeVote(proposalId: number) {
setProposals((prev) =>
prev.map((p) => {
if (p.id === proposalId && p.userVote !== 0) {
const refund = p.userVote * p.userVote;
setCredits((c) => c + refund);
return {
...p,
score: p.score - p.userVote,
userVote: 0,
};
}
return p;
}) })
); );
} }
@ -255,7 +258,7 @@ export default function DemoPage() {
: "bg-muted/50 border-muted text-muted-foreground" : "bg-muted/50 border-muted text-muted-foreground"
}`} }`}
> >
<div className="font-bold text-xl">{w > 0 ? "+" : ""}{w}</div> <div className="font-bold text-xl">+{w}</div>
<div className="text-xs opacity-70">vote{w > 1 ? "s" : ""}</div> <div className="text-xs opacity-70">vote{w > 1 ? "s" : ""}</div>
<div className="font-mono text-sm mt-1 font-semibold">{w * w}¢</div> <div className="font-mono text-sm mt-1 font-semibold">{w * w}¢</div>
</div> </div>
@ -279,80 +282,56 @@ export default function DemoPage() {
const hasPending = proposal.pendingVote !== 0; const hasPending = proposal.pendingVote !== 0;
const hasVoted = proposal.userVote !== 0; const hasVoted = proposal.userVote !== 0;
const pendingCost = proposal.pendingVote * proposal.pendingVote; const pendingCost = proposal.pendingVote * proposal.pendingVote;
const previewScore = proposal.score + proposal.pendingVote; const displayScore = hasPending ? proposal.score + proposal.pendingVote : proposal.score;
const progressPercent = Math.min((proposal.score / 100) * 100, 100); const progressPercent = Math.min((displayScore / 100) * 100, 100);
const previewPercent = Math.min((previewScore / 100) * 100, 100);
const isUpvoted = (hasVoted && proposal.userVote > 0) || proposal.pendingVote > 0;
const isDownvoted = (hasVoted && proposal.userVote < 0) || proposal.pendingVote < 0;
return ( return (
<Card <div
key={proposal.id} key={proposal.id}
className={`transition-all duration-200 overflow-hidden ${ className={`flex rounded-xl border bg-card shadow-sm overflow-hidden transition-all duration-200 ${
hasPending hasPending
? proposal.pendingVote > 0 ? proposal.pendingVote > 0
? "ring-2 ring-orange-500/50 bg-orange-500/5" ? "ring-2 ring-orange-500/50"
: "ring-2 ring-blue-500/50 bg-blue-500/5" : "ring-2 ring-blue-500/50"
: hasVoted
? proposal.userVote > 0
? "bg-orange-500/5"
: "bg-blue-500/5"
: "" : ""
}`} }`}
> >
<div className="flex">
{/* Reddit-style vote column */} {/* Reddit-style vote column */}
<div className="flex flex-col items-center justify-center py-4 px-3 bg-muted/40 min-w-[70px] gap-1"> <div className="flex flex-col items-center justify-center py-3 px-4 bg-muted/50 border-r min-w-[72px]">
{/* Upvote button */} <Button
<button variant="ghost"
onClick={() => { size="sm"
if (hasVoted && proposal.userVote > 0) { className={`h-10 w-10 p-0 rounded-md transition-all ${
removeVote(proposal.id); isUpvoted
} else if (!hasVoted || proposal.pendingVote >= 0) { ? "text-orange-500 bg-orange-500/10 hover:bg-orange-500/20"
incrementVote(proposal.id); : "text-muted-foreground hover:text-orange-500 hover:bg-orange-500/10"
} } ${hasVoted && proposal.userVote < 0 ? "opacity-30 pointer-events-none" : ""}`}
}} onClick={() => handleUpvote(proposal.id)}
disabled={hasVoted && proposal.userVote < 0}
className={`p-1 rounded transition-all hover:scale-110 ${
(hasVoted && proposal.userVote > 0) || proposal.pendingVote > 0
? "text-orange-500"
: "text-muted-foreground hover:text-orange-500"
} ${hasVoted && proposal.userVote < 0 ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
> >
<ChevronUp className="h-8 w-8" strokeWidth={3} /> <ChevronUp className="h-7 w-7" strokeWidth={2.5} />
</button> </Button>
{/* Score display */} <span className={`font-bold text-xl tabular-nums py-1 ${
<div className={`font-bold text-xl tabular-nums min-w-[3ch] text-center ${ isUpvoted ? "text-orange-500" : isDownvoted ? "text-blue-500" : "text-foreground"
hasPending
? proposal.pendingVote > 0
? "text-orange-500"
: "text-blue-500"
: hasVoted
? proposal.userVote > 0
? "text-orange-500"
: "text-blue-500"
: "text-foreground"
}`}> }`}>
{hasPending ? previewScore : proposal.score} {displayScore}
</div> </span>
{/* Downvote button */} <Button
<button variant="ghost"
onClick={() => { size="sm"
if (hasVoted && proposal.userVote < 0) { className={`h-10 w-10 p-0 rounded-md transition-all ${
removeVote(proposal.id); isDownvoted
} else if (!hasVoted || proposal.pendingVote <= 0) { ? "text-blue-500 bg-blue-500/10 hover:bg-blue-500/20"
decrementVote(proposal.id); : "text-muted-foreground hover:text-blue-500 hover:bg-blue-500/10"
} } ${hasVoted && proposal.userVote > 0 ? "opacity-30 pointer-events-none" : ""}`}
}} onClick={() => handleDownvote(proposal.id)}
disabled={hasVoted && proposal.userVote > 0}
className={`p-1 rounded transition-all hover:scale-110 ${
(hasVoted && proposal.userVote < 0) || proposal.pendingVote < 0
? "text-blue-500"
: "text-muted-foreground hover:text-blue-500"
} ${hasVoted && proposal.userVote > 0 ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
> >
<ChevronDown className="h-8 w-8" strokeWidth={3} /> <ChevronDown className="h-7 w-7" strokeWidth={2.5} />
</button> </Button>
</div> </div>
{/* Proposal content */} {/* Proposal content */}
@ -366,29 +345,23 @@ export default function DemoPage() {
<div className="mt-3"> <div className="mt-3">
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1"> <div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
<span>Progress to voting stage</span> <span>Progress to voting stage</span>
<span className={hasPending ? (proposal.pendingVote > 0 ? "text-orange-500" : "text-blue-500") : ""}> <span className={isUpvoted ? "text-orange-500 font-medium" : isDownvoted ? "text-blue-500 font-medium" : ""}>
{hasPending ? previewScore : proposal.score}/100 {displayScore}/100
</span> </span>
</div> </div>
<div className="h-2 rounded-full bg-muted overflow-hidden"> <div className="h-2 rounded-full bg-muted overflow-hidden">
<div <div
className={`h-full rounded-full transition-all duration-300 ${ className={`h-full rounded-full transition-all duration-300 ${
hasPending isUpvoted ? "bg-orange-500" : isDownvoted ? "bg-blue-500" : "bg-primary"
? proposal.pendingVote > 0
? "bg-orange-500"
: "bg-blue-500"
: "bg-primary"
}`} }`}
style={{ width: `${hasPending ? previewPercent : progressPercent}%` }} style={{ width: `${progressPercent}%` }}
/> />
</div> </div>
</div> </div>
{/* Vote status / pending confirmation */} {/* Pending vote confirmation */}
{(hasPending || hasVoted) && ( {hasPending && (
<div className="mt-3 flex items-center gap-3"> <div className="mt-3 flex items-center gap-2">
{hasPending ? (
<>
<Badge <Badge
variant="outline" variant="outline"
className={proposal.pendingVote > 0 className={proposal.pendingVote > 0
@ -401,7 +374,7 @@ export default function DemoPage() {
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="h-7 px-2 text-muted-foreground hover:text-foreground" className="h-7 px-2"
onClick={() => cancelPending(proposal.id)} onClick={() => cancelPending(proposal.id)}
> >
<X className="h-4 w-4 mr-1" /> <X className="h-4 w-4 mr-1" />
@ -414,28 +387,30 @@ export default function DemoPage() {
? "bg-orange-500 hover:bg-orange-600" ? "bg-orange-500 hover:bg-orange-600"
: "bg-blue-500 hover:bg-blue-600" : "bg-blue-500 hover:bg-blue-600"
}`} }`}
onClick={() => castVote(proposal.id)} onClick={() => confirmVote(proposal.id)}
> >
<Check className="h-4 w-4 mr-1" /> <Check className="h-4 w-4 mr-1" />
Confirm Confirm
</Button> </Button>
</> </div>
) : hasVoted && ( )}
{/* Existing vote indicator */}
{hasVoted && !hasPending && (
<div className="mt-3">
<Badge <Badge
variant="secondary" variant="secondary"
className={proposal.userVote > 0 className={proposal.userVote > 0
? "bg-orange-500/20 text-orange-600 border-orange-500/30" ? "bg-orange-500/20 text-orange-600"
: "bg-blue-500/20 text-blue-600 border-blue-500/30" : "bg-blue-500/20 text-blue-600"
} }
> >
You voted: {proposal.userVote > 0 ? "+" : ""}{proposal.userVote} You voted: {proposal.userVote > 0 ? "+" : ""}{proposal.userVote}
</Badge> </Badge>
)}
</div> </div>
)} )}
</div> </div>
</div> </div>
</Card>
); );
})} })}
</div> </div>
@ -468,7 +443,6 @@ export default function DemoPage() {
<CardDescription>{proposal.description}</CardDescription> <CardDescription>{proposal.description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Vote bar */}
<div className="space-y-2"> <div className="space-y-2">
<div className="h-4 rounded-full overflow-hidden bg-muted flex"> <div className="h-4 rounded-full overflow-hidden bg-muted flex">
<div <div
@ -490,7 +464,6 @@ export default function DemoPage() {
</div> </div>
</div> </div>
{/* Vote buttons */}
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
<Button <Button
variant="outline" variant="outline"