From e7d1cb3bf7e0a76831d60190b13efae452e3c5ba Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 22 Feb 2026 17:33:52 -0800 Subject: [PATCH] Vote fixing --- frontend.css | 37 ++++++++++++++++++------------------- frontend.tsx | 33 ++++++++++++++++----------------- server.ts | 14 ++++++++++---- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/frontend.css b/frontend.css index c46bc83..9ec4827 100644 --- a/frontend.css +++ b/frontend.css @@ -335,6 +335,24 @@ body { background: rgba(255, 255, 255, 0.06); } +/* ── My Vote Highlight ──────────────────────────────────────── */ + +.contestant--my-vote { + background: rgba(255, 255, 255, 0.03); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.my-vote-tag { + font-family: var(--mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; + padding: 3px 8px; + border: 1px solid var(--text-muted); + color: var(--text-dim); + border-radius: 3px; +} + /* ── Viewer Votes ────────────────────────────────────────────── */ .viewer-vote-bar { @@ -358,25 +376,6 @@ body { font-size: 14px; } -/* ── Vote CTA ────────────────────────────────────────────────── */ - -.vote-cta { - text-align: center; - font-family: var(--mono); - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; - color: var(--text-dim); - padding: 10px 0; - margin-bottom: 8px; - animation: vote-cta-pulse 2s ease-in-out infinite; -} - -@keyframes vote-cta-pulse { - 0%, 100% { opacity: 0.7; } - 50% { opacity: 1; } -} .vote-countdown { color: var(--text); diff --git a/frontend.tsx b/frontend.tsx index 01eb14b..b7686a1 100644 --- a/frontend.tsx +++ b/frontend.tsx @@ -53,7 +53,7 @@ type ViewerCountMessage = { type: "viewerCount"; viewerCount: number; }; -type VotedAckMessage = { type: "votedAck" }; +type VotedAckMessage = { type: "votedAck"; votedFor: "A" | "B" }; type ServerMessage = StateMessage | ViewerCountMessage | VotedAckMessage; // ── Model colors & logos ───────────────────────────────────────────────────── @@ -163,6 +163,7 @@ function ContestantCard({ totalViewerVotes, votable, onVote, + isMyVote, }: { task: TaskInfo; voteCount: number; @@ -174,6 +175,7 @@ function ContestantCard({ totalViewerVotes?: number; votable?: boolean; onVote?: () => void; + isMyVote?: boolean; }) { const color = getColor(task.model.name); const pct = totalVotes > 0 ? Math.round((voteCount / totalVotes) * 100) : 0; @@ -184,7 +186,7 @@ function ContestantCard({ return (
+ {isMyVote && YOUR PICK} {isWinner && WIN}
@@ -277,13 +280,13 @@ function ContestantCard({ function Arena({ round, total, - hasVoted, + myVote, onVote, viewerVotingSecondsLeft, }: { round: RoundState; total: number | null; - hasVoted: boolean; + myVote: "A" | "B" | null; onVote: (side: "A" | "B") => void; viewerVotingSecondsLeft: number; }) { @@ -304,7 +307,6 @@ function Arena({ const canVote = round.phase === "voting" && - !hasVoted && viewerVotingSecondsLeft > 0 && round.answerTasks[0].finishedAt && round.answerTasks[1].finishedAt; @@ -337,12 +339,6 @@ function Arena({ - {canVote && ( -
- Pick the funnier answer! -
- )} - {round.phase !== "prompting" && (
onVote("A")} + isMyVote={myVote === "A"} /> onVote("B")} + isMyVote={myVote === "B"} />
)} @@ -492,16 +490,16 @@ function App() { const [totalRounds, setTotalRounds] = useState(null); const [viewerCount, setViewerCount] = useState(0); const [connected, setConnected] = useState(false); - const [hasVoted, setHasVoted] = useState(false); + const [myVote, setMyVote] = useState<"A" | "B" | null>(null); const [votedRound, setVotedRound] = useState(null); const [viewerVotingSecondsLeft, setViewerVotingSecondsLeft] = useState(0); const wsRef = React.useRef(null); - // Reset hasVoted when round changes + // Reset vote when round changes useEffect(() => { const currentRound = state?.active?.num ?? null; if (currentRound !== null && currentRound !== votedRound) { - setHasVoted(false); + setMyVote(null); setVotedRound(null); } }, [state?.active?.num, votedRound]); @@ -552,7 +550,7 @@ function App() { } else if (msg.type === "viewerCount") { setViewerCount(msg.viewerCount); } else if (msg.type === "votedAck") { - setHasVoted(true); + setMyVote(msg.votedFor); } }; } @@ -565,8 +563,9 @@ function App() { }, []); const handleVote = (side: "A" | "B") => { - if (hasVoted || !wsRef.current) return; + if (myVote === side || !wsRef.current) return; wsRef.current.send(JSON.stringify({ type: "vote", votedFor: side })); + setMyVote(side); setVotedRound(state?.active?.num ?? null); }; @@ -607,7 +606,7 @@ function App() { diff --git a/server.ts b/server.ts index c207c86..f4f0095 100644 --- a/server.ts +++ b/server.ts @@ -218,7 +218,7 @@ function setHistoryCache(key: string, body: string, expiresAt: number) { // ── WebSocket clients ─────────────────────────────────────────────────────── const clients = new Set>(); -const viewerVoters = new Set>(); +const viewerVoters = new Map, "A" | "B">(); let viewerVoteBroadcastTimer: ReturnType | null = null; function scheduleViewerVoteBroadcast() { @@ -611,14 +611,20 @@ const server = Bun.serve({ const round = gameState.active; if (!round || round.phase !== "voting") return; if (!round.viewerVotingEndsAt || Date.now() > round.viewerVotingEndsAt) return; - if (viewerVoters.has(ws)) return; if (msg.votedFor !== "A" && msg.votedFor !== "B") return; - viewerVoters.add(ws); + const previousVote = viewerVoters.get(ws); + if (previousVote === msg.votedFor) return; // same vote, ignore + + // Undo previous vote if changing + if (previousVote === "A") round.viewerVotesA = Math.max(0, (round.viewerVotesA ?? 0) - 1); + else if (previousVote === "B") round.viewerVotesB = Math.max(0, (round.viewerVotesB ?? 0) - 1); + + viewerVoters.set(ws, msg.votedFor); if (msg.votedFor === "A") round.viewerVotesA = (round.viewerVotesA ?? 0) + 1; else round.viewerVotesB = (round.viewerVotesB ?? 0) + 1; - ws.send(JSON.stringify({ type: "votedAck" })); + ws.send(JSON.stringify({ type: "votedAck", votedFor: msg.votedFor })); scheduleViewerVoteBroadcast(); } catch {} },