Skip to content
This repository was archived by the owner on Mar 16, 2026. It is now read-only.

Commit bc918f4

Browse files
authored
Merge pull request #723 from aibtcdev/1
display quorum and threshold in main card
2 parents e6af0d7 + 0623c1b commit bc918f4

9 files changed

Lines changed: 298 additions & 58 deletions

File tree

src/components/proposals/ProposalCard.tsx

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { useProposalVote } from "@/hooks/useProposalVote";
1616
import { Button } from "@/components/ui/button";
1717
import { RefreshCw, AlertCircle } from "lucide-react";
1818
import { motion } from "framer-motion";
19+
import { safeNumberFromBigInt } from "@/utils/proposal";
20+
import { cn } from "@/lib/utils";
1921

2022
interface ProposalCardProps {
2123
proposal: Proposal | ProposalWithDAO;
@@ -31,14 +33,16 @@ export default function ProposalCard({
3133
const router = useRouter();
3234

3335
// Use the unified status system
34-
const { statusConfig, isActive, isPassed } = useProposalStatus(proposal);
36+
const { status, statusConfig, isActive } = useProposalStatus(proposal);
3537

3638
// Use centralized vote hook for consistent data fetching
3739
const {
3840
voteDisplayData,
41+
calculations,
3942
error: hasVoteDataError,
4043
refreshVoteData,
4144
isLoading: isLoadingVotes,
45+
vetoCheck,
4246
} = useProposalVote({
4347
proposal,
4448
contractPrincipal: proposal.contract_principal,
@@ -90,6 +94,43 @@ export default function ProposalCard({
9094
// const liquidTokens = Number(proposal.liquid_tokens);
9195
const { totalVotes, hasVoteData } = voteSummary;
9296

97+
// Enhanced calculations for quorum and threshold display
98+
const enhancedCalculations = useMemo(() => {
99+
if (!calculations) return null;
100+
101+
const quorumPercentage = safeNumberFromBigInt(proposal.voting_quorum);
102+
const thresholdPercentage = safeNumberFromBigInt(proposal.voting_threshold);
103+
104+
// Calculate if requirements are met
105+
const metQuorum = calculations.participationRate >= quorumPercentage;
106+
const metThreshold =
107+
calculations.totalVotes > 0
108+
? calculations.approvalRate >= thresholdPercentage
109+
: false;
110+
111+
return {
112+
...calculations,
113+
quorumPercentage,
114+
thresholdPercentage,
115+
metQuorum,
116+
metThreshold,
117+
};
118+
}, [calculations, proposal]);
119+
120+
// Helper function for status display
121+
const getStatusText = (met: boolean, percentage?: number) => {
122+
// If voting hasn't started (PENDING, DRAFT), show "Pending"
123+
if (status === "PENDING" || status === "DRAFT") {
124+
return "Pending";
125+
}
126+
127+
if (isActive) {
128+
return percentage !== undefined ? `${percentage.toFixed(1)}%` : "0%";
129+
}
130+
131+
return met ? "Passed" : "Failed";
132+
};
133+
93134
// Memoize DAO info
94135
const daoInfo = useMemo(() => {
95136
const proposalWithDAO = proposal as ProposalWithDAO;
@@ -130,12 +171,93 @@ export default function ProposalCard({
130171
? `#${proposal.proposal_id}: ${proposal.title}`
131172
: proposal.title}
132173
</h3>
133-
<div className="flex items-center gap-2 sm:gap-3 mb-2">
174+
<div className="flex flex-wrap items-center gap-2 mb-2">
134175
<ProposalStatusBadge
135176
proposal={proposal}
136177
size="sm"
137178
className="flex-shrink-0"
138179
/>
180+
181+
{/* Quorum and Threshold Badges */}
182+
{enhancedCalculations &&
183+
statusConfig.label !== "Pending" &&
184+
statusConfig.label !== "Draft" && (
185+
<>
186+
{/* Quorum Badge */}
187+
<div
188+
className={cn(
189+
"px-2 py-0.5 rounded-sm text-xs font-medium border flex-shrink-0",
190+
isActive
191+
? enhancedCalculations.metQuorum
192+
? "bg-success/10 border-success/20"
193+
: "bg-primary/10 border-primary/20"
194+
: enhancedCalculations.metQuorum
195+
? "bg-success/10 border-success/20"
196+
: "bg-destructive/10 border-destructive/20"
197+
)}
198+
>
199+
<span className="text-muted-foreground">Quorum:</span>{" "}
200+
<span
201+
className={cn(
202+
isActive
203+
? enhancedCalculations.metQuorum
204+
? "text-success"
205+
: "text-primary"
206+
: enhancedCalculations.metQuorum
207+
? "text-success"
208+
: "text-destructive"
209+
)}
210+
>
211+
{getStatusText(
212+
enhancedCalculations.metQuorum,
213+
enhancedCalculations.participationRate
214+
)}
215+
</span>
216+
</div>
217+
218+
{/* Threshold Badge */}
219+
<div
220+
className={cn(
221+
"px-2 py-0.5 rounded-sm text-xs font-medium border flex-shrink-0",
222+
isActive
223+
? enhancedCalculations.metThreshold
224+
? "bg-success/10 border-success/20"
225+
: "bg-primary/10 border-primary/20"
226+
: enhancedCalculations.metThreshold
227+
? "bg-success/10 border-success/20"
228+
: "bg-destructive/10 border-destructive/20"
229+
)}
230+
>
231+
<span className="text-muted-foreground">
232+
Threshold:
233+
</span>{" "}
234+
<span
235+
className={cn(
236+
isActive
237+
? enhancedCalculations.metThreshold
238+
? "text-success"
239+
: "text-primary"
240+
: enhancedCalculations.metThreshold
241+
? "text-success"
242+
: "text-destructive"
243+
)}
244+
>
245+
{getStatusText(
246+
enhancedCalculations.metThreshold,
247+
enhancedCalculations.approvalRate
248+
)}
249+
</span>
250+
</div>
251+
</>
252+
)}
253+
254+
{/* Veto Override Warning Badge */}
255+
{vetoCheck?.vetoExceedsForVote && !isActive && (
256+
<div className="px-2 py-0.5 rounded-sm text-xs font-medium border flex-shrink-0 bg-destructive/10 border-destructive/20">
257+
<span className="text-destructive">⚠️ Vetoed</span>
258+
</div>
259+
)}
260+
139261
<div className="flex items-center gap-1 text-xs text-foreground/75 flex-shrink-0">
140262
<Clock className="h-3 w-3 flex-shrink-0" />
141263
<span className="whitespace-nowrap">
@@ -347,28 +469,11 @@ export default function ProposalCard({
347469
</div>
348470
)}
349471

350-
{/* Completed Status */}
351-
{/* {isPassed && (
352-
<div className="text-sm">
353-
<span className="text-foreground/75">Final result: </span>
354-
<span className="font-medium">
355-
<span className="text-success">
356-
<TokenBalance variant="abbreviated" value={votesFor} /> For
357-
</span>
358-
,{" "}
359-
<span className="text-destructive">
360-
<TokenBalance variant="abbreviated" value={votesAgainst} />{" "}
361-
Against
362-
</span>
363-
</span>
364-
</div>
365-
)} */}
366-
367-
{/* Enhanced Chart Section for detailed view - Hide for pending proposals */}
472+
{/* Vote Status Chart - Show for active, veto period, execution window, passed, and failed proposals */}
368473
{(isActive ||
369474
statusConfig.label === "Veto Period" ||
370475
statusConfig.label === "Execution Window" ||
371-
isPassed ||
476+
statusConfig.label === "Passed" ||
372477
statusConfig.label === "Failed") &&
373478
statusConfig.label !== "Pending" && (
374479
<div className="">

src/components/proposals/ProposalSubmission.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,28 @@ export function ProposalSubmission({
13191319
</div>
13201320
</div>
13211321
)}
1322+
1323+
{/* Blocked User Lock Overlay */}
1324+
{hasAccessToken &&
1325+
!needsXLink &&
1326+
!isXLoading &&
1327+
profile?.is_blocked === true && (
1328+
<div className="absolute inset-0 bg-zinc-900 rounded-sm flex flex-col items-center justify-center z-10">
1329+
<div className="text-center space-y-4 max-w-md mx-auto px-6">
1330+
<div className="w-16 h-16 rounded-sm bg-red-900/20 border border-red-800/30 flex items-center justify-center mx-auto">
1331+
<Lock className="w-8 h-8 text-red-400" />
1332+
</div>
1333+
<div>
1334+
<h3 className="text-xl font-bold text-red-300 mb-2">
1335+
Account Restricted
1336+
</h3>
1337+
<p className="text-sm text-red-200/80 leading-relaxed">
1338+
Your account is restricted from submitting contributions.
1339+
</p>
1340+
</div>
1341+
</div>
1342+
</div>
1343+
)}
13221344
{/* Airdrop notification - Commented out per user request */}
13231345
{/* {hasAccessToken && (
13241346
<div className="bg-secondary/40 rounded-sm p-3 shadow-sm">
@@ -1694,7 +1716,8 @@ export function ProposalSubmission({
16941716
isLoadingEmbed ||
16951717
!twitterEmbedData ||
16961718
!!xUsernameError ||
1697-
!canSubmitContribution
1719+
!canSubmitContribution ||
1720+
profile?.is_blocked === true
16981721
}
16991722
className="bg-primary hover:bg-primary/90 text-primary-foreground font-bold px-6 py-2 text-sm rounded-sm shadow-md hover:shadow-lg transition-all duration-200"
17001723
>
@@ -1746,6 +1769,11 @@ export function ProposalSubmission({
17461769
<Loader />
17471770
<span className="break-words">Loading Post Content...</span>
17481771
</div>
1772+
) : profile?.is_blocked === true ? (
1773+
<div className="flex items-center gap-2">
1774+
<Lock className="w-4 h-4" />
1775+
<span>Account Restricted</span>
1776+
</div>
17491777
) : (
17501778
<div className="flex items-center gap-2">
17511779
<Send className="h-4 w-4" />

src/components/proposals/VoteStatusChart.tsx

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"use client";
22

3-
import { useState, useCallback } from "react";
3+
import { useState, useCallback, useMemo } from "react";
44
import { TokenBalance } from "@/components/reusables/BalanceDisplay";
55
import { Button } from "@/components/ui/button";
66
import { RefreshCw, AlertCircle } from "lucide-react";
77
import { useProposalVote } from "@/hooks/useProposalVote";
8+
import { safeNumberFromBigInt } from "@/utils/proposal";
89
import type { Proposal, ProposalWithDAO } from "@/types";
910

1011
interface VoteStatusChartProps {
@@ -22,7 +23,7 @@ const VoteStatusChart = ({
2223
initialVotesAgainst,
2324
// refreshing = false,
2425
tokenSymbol = "",
25-
liquidTokens,
26+
// liquidTokens,
2627
proposal,
2728
}: VoteStatusChartProps) => {
2829
const [localRefreshing, setLocalRefreshing] = useState(false);
@@ -62,6 +63,34 @@ const VoteStatusChart = ({
6263
[refreshVoteData]
6364
);
6465

66+
// Calculate progress bar size based on quorum (must be before early returns)
67+
const progressBarCalculations = useMemo(() => {
68+
if (!proposal || !calculations) return null;
69+
70+
const quorumPercentage = safeNumberFromBigInt(proposal.voting_quorum);
71+
const liquidTokensNum = calculations.liquidTokensNum;
72+
73+
// Calculate quorum amount in tokens
74+
const quorumAmount = (liquidTokensNum * quorumPercentage) / 100;
75+
76+
// Bar width is quorum + 10%, or total votes if exceeded
77+
const barWidth = Math.max(quorumAmount * 1.1, calculations.totalVotes);
78+
79+
// Calculate percentages based on the bar width
80+
const votesForPercentage = (calculations.votesForNum / barWidth) * 100;
81+
const votesAgainstPercentage =
82+
(calculations.votesAgainstNum / barWidth) * 100;
83+
const quorumLinePercentage = (quorumAmount / barWidth) * 100;
84+
85+
return {
86+
votesForPercentage,
87+
votesAgainstPercentage,
88+
quorumLinePercentage,
89+
quorumAmount,
90+
barWidth,
91+
};
92+
}, [proposal, calculations]);
93+
6594
// Show loading state
6695
if (isLoadingVotes && !error) {
6796
return (
@@ -100,7 +129,7 @@ const VoteStatusChart = ({
100129
);
101130
}
102131

103-
if (!voteDisplayData || !calculations) {
132+
if (!voteDisplayData || !calculations || !progressBarCalculations) {
104133
return (
105134
<div className="flex items-center justify-center p-4">
106135
<span className="text-sm text-muted-foreground">
@@ -110,8 +139,6 @@ const VoteStatusChart = ({
110139
);
111140
}
112141

113-
// const isRefreshingAny = localRefreshing || refreshing;
114-
115142
// Main vote display
116143
return (
117144
<div className="space-y-2">
@@ -122,17 +149,29 @@ const VoteStatusChart = ({
122149
<div
123150
className="absolute left-0 top-0 h-full bg-green-500/80 transition-all duration-500 ease-out rounded-l-full"
124151
style={{
125-
width: `${Math.min(calculations.barPercentageFor, 100)}%`,
152+
width: `${Math.min(progressBarCalculations.votesForPercentage, 100)}%`,
126153
}}
127154
/>
128155
{/* Votes against (red) */}
129156
<div
130157
className="absolute top-0 h-full bg-red-500/80 transition-all duration-500 ease-out"
131158
style={{
132-
width: `${Math.min(calculations.barPercentageAgainst, 100)}%`,
133-
left: `${Math.min(calculations.barPercentageFor, 100)}%`,
159+
width: `${Math.min(progressBarCalculations.votesAgainstPercentage, 100)}%`,
160+
left: `${Math.min(progressBarCalculations.votesForPercentage, 100)}%`,
134161
}}
135162
/>
163+
{/* Quorum line indicator */}
164+
<div
165+
className="absolute top-0 bottom-0 w-0.5 bg-primary z-10"
166+
style={{
167+
left: `${Math.min(progressBarCalculations.quorumLinePercentage, 100)}%`,
168+
}}
169+
>
170+
<div
171+
className="absolute -top-1 w-3 h-3 bg-primary rounded-sm border-2 border-background"
172+
style={{ left: "-5px" }}
173+
/>
174+
</div>
136175
</div>
137176
</div>
138177

@@ -171,26 +210,6 @@ const VoteStatusChart = ({
171210
className="font-medium sm:hidden"
172211
/>
173212
</div>
174-
175-
{/* Liquid Tokens - Right */}
176-
{liquidTokens && Number(liquidTokens) > 0 && (
177-
<div className="flex items-center gap-1">
178-
<span className="text-muted-foreground">Liquid Token:</span>
179-
<TokenBalance
180-
value={liquidTokens}
181-
decimals={8}
182-
variant="abbreviated"
183-
symbol={tokenSymbol}
184-
className="font-medium hidden sm:inline"
185-
/>
186-
<TokenBalance
187-
value={liquidTokens}
188-
decimals={8}
189-
variant="abbreviated"
190-
className="font-medium sm:hidden"
191-
/>
192-
</div>
193-
)}
194213
</div>
195214
</div>
196215
);

src/components/proposals/VotesTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,9 @@ const VotesTable = ({ proposalId, limit }: VotesTableProps) => {
170170
</tr>
171171
</thead>
172172
<tbody>
173-
{displayedVotes.map((vote, index) => (
173+
{displayedVotes.map((vote) => (
174174
<tr
175-
key={vote.tx_id || index}
175+
key={vote.id}
176176
className="border-b border-border/50 hover:bg-muted/30 transition-colors"
177177
>
178178
{/* Voter */}

0 commit comments

Comments
 (0)